Transformer, Positional encoding, Self-Attention
들어가기에 앞서, 오늘 배운 transformer 개념이 세션 영상이나 노트만으로는 이해하기가 어려워서 다른 영상도 찾아보았었다.
Transformer란 어제 배운 Attention 매커니즘을 극대화한 기계 번역을 위한 새로운 모델이다. 매우 높은 성능을 자랑하여 최근 자연어 처리 모델 SOTA(State-of-Art)의 기본 아이디어는 모두 이 트랜스포머를 기반으로 하고 있다고 한다. SOTA(State-of-Art)란? 현재 최고 수준의 결과라고 한다. 즉, 위 말을 풀어보면 '최근 자연어 처리에서 최고 성능을 보이는 모델의 기본 아이디어는 모두 이 트랜스포머를 기반으로 하고 있다.'고 할 수 있겠다.)Transformer의 장점에 대해 얘기하기 위해 이전에 배운 RNN의 단점에 대해 먼저 얘기해보자.

왜 TIL 제목에 Attention is All You Need를 넣어놨냐면 구글에서 트랜스포머 모델을 제안한 논문의 제목이기 때문이다.
그럼 해당 논문에서 제시한 구조 이미지를 한 번 보도록 하자.

인코더 블록이고 그 오른쪽이 디코더 블록이다. 각각 옆에 N x라고 들어가 있는 건 저게 딱 하나만 있는게 아니라 여러 개 있을 수 있다는 뜻이다. (논문에서는 6개로 제안했는데, 강필성 교수님에 의하면 이게 magic number는 아니라고 한다. 꼭 6개여야만 하는 논리적인 이유는 없다는 뜻)
인코더 블록은 크게 Multi-Head (self) Attention과 Feed Forward 두 개의 layer로 이루어져있다. 반면 디코더 블록은 Masked Multi-head (self) Attention과 Multi-head (Encoder-Decoder) Attention, Feed Forward 3개의 layer로 이루어져 있다.자, 구조에 대해서는 다음 목차에서 좀 더 본격적으로 다뤄보도록 하겠다.
하나하나 전부 깊이있게 짚고 넘어가려고 하기보단 우선 오늘은 어떤 역할을 하는지를 기억하는 느낌으로 보자고~!

Positional encoding이다.Positional encoding은 단어의 상대적인 위치 정보를 담은 벡터를 만드는 과정을 말한다. 수학적으로는 아래와 같이 식이 이루어져 있다고는 하는데.. 수식을 이해하려고 하기보단 우선 왜 필요하고 뭔지 정도만 알고 넘어가도 된다고 한다. (sin, cos을 사용하는 방법이 있다 정도는 기억해두자)
def get_angles(pos, i, d_model):
"""
sin, cos 안에 들어갈 수치를 구하는 함수입니다.
"""
angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
return pos * angle_rates
def positional_encoding(position, d_model):
"""
위치 인코딩(Positional Encoding)을 구하는 함수입니다.
"""
angle_rads = get_angles(np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model)
# apply sin to even indices in the array; 2i
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
# apply cos to odd indices in the array; 2i+1
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
pos_encoding = angle_rads[np.newaxis, ...]
return tf.cast(pos_encoding, dtype=tf.float32)
이렇게 Positional Encoding과 Input Embeding이 더해져 인코더 블록으로 들어가게 된다. 참고로 여기서 +는 행렬의 concat이 아니라 그냥 요소끼리 더하는 걸 말한다! 휴~ 하나 끝!
트랜스포머의 주요 매커니즘으로 오늘은 사실상 다른 건 다 몰라도 이 self attention 하나라도 제대로 알고 넘어가자고 했다. (미리 말하자면, 시간이 된다면 위 강필성 교수님의 영상을 보는게 아래 글보다 훨씬 이해할 때 도움이 될 거라는 점..)
다음과 같은 문장이 있다고 해보자.
The animal didn't cross the street because it was too tired
it이 the animal이라는 걸 우리 사람은 직관적으로 알 수 있지만, 컴퓨터는 그렇지 않다.
Query, Key, Value이다. 아래 그림을 보자. (출처 - 강필성 교수님 영상)

it)에 대한 쿼리와 각 단어들의 키를 통해 Score를 계산한다. 아래처럼! 이건 attention에서처럼 어떤 단어에 더 주목할지를 결정하는 과정으로 생각하면 된다. 즉, 쿼리행렬과 각 키 행렬 간의 내적 및 softmax 함수를 거쳐 확률값을 나타낸다는 뜻이다.



정리해보니 생각보다 이해하기 어렵진 않지..?
자, self-attention 매커니즘 정리!
- 특정 단어의 쿼리(q) 벡터와 모든 단어의 키(k) 벡터를 내적한다.
(내적을 통해 나오는 값이 Attention 스코어(Score)가 된다)- 이 가중치를 q,k,v 벡터 차원 의 제곱근인 로 나누어준다.
(계산값을 안정적으로 만들어주기 위한 계산 보정)- Softmax를 취해준다.
이를 통해 쿼리에 해당하는 단어와 문장 내 다른 단어가 가지는 관계의 비율을 구할 수 있다.- 마지막으로 밸류(v) 각 단어의 벡터를 곱해준 후 모두 더한다.
def scaled_dot_product_attention(q, k, v, mask):
"""
Attention 가중치를 구하는 함수입니다.
q, k, v 의 leading dimension은 동일해야 합니다.
k, v의 penultimate dimension이 동일해야 합니다, i.e.: seq_len_k = seq_len_v.
Mask는 타입(padding or look ahead)에 따라 다른 차원을 가질 수 있습니다.
덧셈시에는 브로드캐스팅 될 수 있어야합니다.
Args:
q: query shape == (..., seq_len_q, depth)
k: key shape == (..., seq_len_k, depth)
v: value shape == (..., seq_len_v, depth_v)
mask: Float tensor with shape broadcastable
to (..., seq_len_q, seq_len_k). Defaults to None.
Returns:
output, attention_weights
"""
matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)
# matmul_qk(쿼리와 키의 내적)을 dk의 제곱근으로 scaling 합니다.
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
# 마스킹을 진행합니다.
if mask is not None:
scaled_attention_logits += (mask * -1e9)
# 소프트맥스(softmax) 함수를 통해서 attention weight 를 구해봅시다.
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k)
output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v)
return output, attention_weights
이제는 이런 single attention이 모여있는 형태인 Multi-head Attention으로 넘어가보자구~~
Multi-Head Attention은 위에서 본 여러 개의 Attention 메커니즘을 동시에 병렬적으로 실행하는 것을 말한다. 각 Head마다 다른 Attention 결과를 내어주기 때문에 앙상블과 유사한 효과를 얻을 수 있으며, 병렬화 효과를 극대화 할 수 있다고 한다. 이 말의 의미는 아래 그림을 보는게 이해하기 더 나을 듯.


다른 그림으로 보면 이것과 같다. (논문에서는 8개의 attention을 사용했다고 함) 
참고로 입력된 임베딩 벡터의 차원과 multi-head attention을 거쳐 나온 출력 벡터의 차원이 같다는 것 위 이미지에서도 보여주고 있는데 기억! (이 인코더 차원이 유지되어 다음 인코더로 넘어가야 계산이 가능하다고 함. 그냥 이렇구나~ 의미만 알아둬도 지금은 될 것 같다.)
정리하면 이 이미지로 표현할 수 있다!

Layer normalization의 효과는 Batch normalization과 유사하며, 학습이 훨씬 빠르고 잘 되도록 한다. Skip connection(혹은 Residual connection)은 역전파 과정에서 정보가 소실되지 않도록 한다.이것들에 대해서는 다음 스프린트 때 자세히 배울 거라고 하니 우선 패스!
Masked가 붙어있다는 것에 주목해야 한다. 나머지 과정은 위에서 봤던 Multi-head attention과 똑같다. -1e9))
위처럼 마스킹하는 과정이 들어간다는 것을 제외하고는 multi-head attention과 동일하다는 점. Cheating을 막기 위해 그런거라는 점을 잘 기억하면 될 것 같다.
Multi-head (self) Attention이었는데 이번엔 self 대신 (Encoder-Decoder)라는 단어가 들어가 있다! 무슨 뜻일까?
인코더의 첫번째 서브층 : Query = Key = Value
디코더의 첫번째 서브층 : Query = Key = Value
디코더의 두번째 서브층 : Query : 디코더 행렬 / Key = Value : 인코더 행렬
오늘 노트도 어제와 마찬가지로 노트에 있는 구현 코드를 다 이해하는 것보다는 개념 자체를 잘 이해하는 것이 훨씬 중요하다는 것을 강조했다.
실습 과제는 주로 구현 함수가 주어지고 일부 내용을 채우는거였는데, 그걸 그대로 옮겨두는 건 의미가 없는 것같고, 나중에 시간을 갖고 코드 파헤치는 시간을 겨봐야겠다.