어텐션이란, 각 토큰이 다른 토큰에게 얼마나 관심있는지를 의미합니다. 관심 정도를 알기 위하여 관심도를 점수로 계산하는데 이를 어텐션 스코어라고 부릅니다.
셀프 어텐션이란, 어텐션 스코어로 원본 토큰을 갱신하는 것을 의미합니다. 다른 말로 어텐션 스코어로 가중합(Weighted sum)했다고 표현하기도 합니다. 예를 들어 “The prepare left the station on time.” 문장에서 Station은 그 자체로는 (우주)정거장인지, (버스)정거장인지, 어떤 정거장인지 알 길이 없습니다. 이를 알기 위해 다른 토큰들간의 관심도인 어텐션 스코어를 계산합니다. 그 후 어텐션 스코어로 원본 토큰을 가중합하면 갱신된 토큰이 됩니다. 갱신후엔 (기차)정거장 이라는 의미가 반영된 Station으로 변하게 됩니다.
극단적으로 표현하면 아래의 그림과 같습니다. 기존 토큰 임베딩은 토큰간의 관계가 고정되었지만, 어텐션은 단어간 관계를 서로 유기적으로 조정하여 갱신합니다.
어텐션이란, 토큰간 관심도를 의미하고, 어텐션 스코어는 토큰간 관심도를 계산한 점수이며, 이를 통해 토큰을 Update하는 것을 셀프어텐션이라고 설명드렸습니다. 셀프 어텐션을 진행하면 각 모든 토큰들간 스코어를 계산하고 모든 토큰들의 값이 변경됩니다.
아래 예시 코드를 따라 어텐션에 대하여 알아보겠습니다.
1. 문장 임베딩.
입력받은 문장을 임베딩하여 모델에 주입할 수 있도록 변환해줍니다.
import tensorflow as tf
import numpy as np# 원본문장
sentence = np.array(["the", "train", "station", "left", "the", "station", "on", "time"])
# 임베딩
lookup = tf.keras.layers.StringLookup(vocabulary=np.distinctive(sentence))
emb = tf.keras.layers.Embedding(len(np.distinctive(sentence))+1, 3)
# 출력
print(emb(lookup(sentence)).numpy())
2. 어텐션 스코어 matrix 계산
어텐션 점수는 Dot product로 토큰 벡터간 연관도를 계산할 수 있습니다. 이후 np.sqrt(nums of embedding dims)
로 나눠주고 softmax
해준 결과입니다.
def softmax(x):
return np.exp(x)/sum(np.exp(x))num_emb_dims = 3
matrix = softmax(np.dot(token, token.T)/np.sqrt(num_emb_dims))
pd.DataFrame(matrix)
3. 어텐션 스코어 plotting
아직 학습이 되기 전의 임베딩이라 플로팅을 했을때 인사이트를 얻을 순 없는데, 학습이 진행되고 나면 대각성분과 관심도가 높은 토큰간 색이 짙게 표현됩니다.
import numpy as np
import matplotlib.pyplot as pltfig, ax = plt.subplots()
cax = ax.imshow(matrix, cmap='hot_r', interpolation='nearest') # Utilizing reversed scorching colormap
fig.colorbar(cax)
# Set the ticks and labels for each axes
phrases = ["the", "train", "left", "the", "station", "on", "time"]
ax.set_xticks(np.arange(len(phrases)))
ax.set_yticks(np.arange(len(phrases)))
ax.set_xticklabels(phrases)
ax.set_yticklabels(phrases)
# Rotate the tick labels for x-axis to keep away from overlapping
plt.setp(ax.get_xticklabels(), rotation=45, ha="proper", rotation_mode="anchor")
plt.present()
4. 셀프 어텐션 후 갱신된 토큰 계산.
3번에서 계산된 어텐션 스코어를 원본 토큰에 가중합을 하면 셀프어텐션 결과를 얻을 수 있습니다.
new_token = []
for pivot in vary(matrix.form[0]):
new_token.append(np.sum(token * np.expand_dims(matrix[:,pivot], axis=1), axis=0))
pd.DataFrame(new_token)
0번째 토큰으로 자세히 설명해보면 0번째 토큰의 어텐션 스코어를 기준으로 원본 토큰 임베딩에 element-wise
곱을 하고, 샘플 축을
따라 더하면 새로운 토큰이 계산됩니다.
의사코드로 표현하면 아래와 같습니다.
def self_attention(input_sequence):
output = np.zeros(form=input_sequence.form)
for i, pivot_vector in enumerate(input_sequence):
scores = np.zeros(form=(len(input_sequence),))
for j, vector in enumerate(input_sequence):
dot_result = np.dot(pivot_vector, vector.T) # 어텐션 계산
scores[j] = dot_result
matrix[i][j] = dot_result
scores /= np.sqrt(input_sequence.form[1])
scores = softmax(scores)new_pivot_representation = np.zeros(form=pivot_vector.form)
for j, vector in enumerate(input_sequence):
new_pivot_representation += vector * scores[j] # 셀프 어텐션
output[i] = new_pivot_representation
return output
일반적인 어텐션레이어는 그림과 같습니다.
보통의 레이어에서 입력이 1개지만 어텐션 레이어에서는 3개의 입력을 받습니다. 그 이유는 어텐션 메커니즘에서 입력이 세개이기 때문입니다. 의사코드를 한줄로 표현하면 아래와 같은데요.
outputs = sum(inputs * pairwise_score(inputs, inputs))
inputs이 세번 사용되는데, 모두 같을 필요가 없습니다.
outputs = sum(values * pairwise_score(question, key))
inputs들의 이름을 달리하면 Question, Key, Value라고 부릅니다.
쿼리(Question)는 현재 처리 중인 입력 요소에 대한 정보를 담고 있습니다. 이는 어텐션을 구할 때 “어떤 정보를 찾고 있는가?”에 해당하는 역할을 합니다.
키(Key)는 입력 시퀀스의 각 요소에 대한 식별자 역할을 합니다. 키는 각 요소가 다른 요소들과 비교될 때 사용되며, “어디에서 찾을 것인가?”에 해당하는 역할을 합니다.
값(Worth)은 실제로 검색되고 나서 반환될 정보입니다. 이는 어텐션 메커니즘이 선택한 정보를 바탕으로 최종 출력값을 생성할 때 사용됩니다. “찾은 정보는 무엇인가?”에 해당합니다.
일반적으로 키와 값이 같은 경우가 많고 아래와 같은 상황에 따라 다르기도 합니다.
- 검색 시스템: 쿼리: 검색어 / 키: 상품마다 정의된 검색 키워드 (검색어 사전) / 값: 상품자체
- 번역: 쿼리: 번역된 문장 / 키: 번역할 문장 / 값: 번역할 문장
- 문장 분류: 쿼리, 키, 값 모두 동일
입력 어텐션 레이어가 두개 이상인 것을 멀티헤드라고 부릅니다.
쿼리, 키, 값은 각각의 Dense layer를 통해 독립적인 백터가 됩니다. 여러 개의 어텐션 메커니즘(헤드)을 병렬로 사용하여 서로 다른 부분의 정보를 동시에 학습합니다. 이를 통해 모델은 다양한 관점에서 데이터를 분석할 수 있게 됩니다. CNN 구조에서 커널의 개수를 늘릴 수록 병렬로 학습하여 표현력을 높이는것과 유사합니다.
트랜스포머 인코더와 디코더를 활용한 Seq2Seq 모델은 다른 포스트에서 정리하겠습니다. 어텐션과 어텐션 매커니즘을 이해하는데 조금이나마 도움이 되었으면 좋겠습니다.