이전 글에 이어서 정리하는 글입니다.
앞의 글에서는 트랜스포머의 구조와 셀프 어텐션에 대해서 알아보았습니다.
멀티 헤드 어텐션(Multi-head Attention)
앞서 배운 어텐션에서는 $d_{model}$의 차원을 가진 단어를 num_heads로 나눈 차원을 가지는 $Vector_{Q,K,V}$로 바꾸고 어텐션을 수행했는데, 왜 어텐션을 $d_{model}$로 하지않고, 굳이 num_heads로 나눈 크기만큼의 차원을 축소했을까요?
한번의 어텐션을 하는것 보다 여러번의 어텐션을 병렬로 사용하는 것이 더 효율적 이라고 합니다.어딘가를 감시할때 혼자 감시하는 것보다 여러명이 동시에 감시하는것이 더욱더 정확하게 하는것 처럼말이죠..
그래서 $d_{model}$의 차원을 num_heads로 나눠서 $d_{model}$/num_heads의 차원을 가지는 Q,K,V에 대해서 num_heads개의 병렬 어텐션이 이루어져 집니다.이때 num_heads개의 만큼 어텐션이 병렬로 이루어지는데, 각각의 어텐션 값 행렬을 어텐션 헤드라고 합니다.이때 가중치 행렬 $W^Q,W^K,W^V$의 값은 어텐션 헤드마다 전부 다릅니다.
병렬 어텐션을 모두 수행하면 어텐션 헤드를 연결합니다. 모두 연결된 어텐션 헤드 행렬크기는 (seq_len,$d_{model}$)이 되겠죠?
어텐션 헤드를 모두 연결한 행렬은 또 다른 가중치 행렬 $W^0$을 곱하게 되는데, 이렇게 나온 결과 행렬이 멀티-헤드 어텐션의 최종 결과물 입니다. 위의 그림은 가중치 $W^0$와의 곱 과정을 보여줍니다.
이때 결과물인 멀티-헤드 어텐션 행렬은 인코더의 입력이었던 문장 행렬인 (seq_len, $d_{model}$)과 동일
- 멀티-헤드 어텐션 단계가 끝났을때, 인코더의 입력으로 들어왔던 행렬의 크기가 아직 유지
- 첫번째 서브층인 멀티-헤드 어텐션과 두번째 서브층인 포지션 와이드 피드포워드 신경망을 지나면서 인코더의 입력으로 들어올때의 행렬의 크기는 계속 유지되어야함
- 트랜스포머는 동일한 구조의 인코더를 쌓은 구조기 때문에, 인코더에서의 입력의 크기가 출력에서도 동일 크기로 유지되어야만 다음 인코더의 입력으로 사용 될 수 있음
패딩 마스크(Padding Mask)
이전 글에서 스케일드 닷-프로덕트 어텐션 함수 내부에 mask 라는 값을 인자로 받고 , -1e9라는 아주 작은 음수값을 곱한후 어텐션 행렬에 더해주고 있습니다. 이 연산은 도대체 뭘까요?
def scaled_dot_product_attention(query, key, value, mask):
... 중략 ...
logits += (mask * -1e9) # 어텐션 스코어 행렬인 logits에 mask*-1e9 값을 더해주고 있다.
... 중략 ...
바로, 입력문자에 <PAD> 토큰이 있을 경우 어텐션에서 제외하기위한 연산입니다.
이 연산을 살펴보면, <PAD>의 경우는 실질적인 의미를 가진 단어가 아닙니다. 그래서 트랜스포머는 Key의 경우 <PAD> 토큰이 존재한다면 이에 대해서는 유사도를 구하지 않도록 마스킹(Masking)을 합니다.
※여기서 마스킹이란 어텐션에서 제외하기 위해 값을 가린다 라는 의미
어텐션 스코어행렬에서 행에 해당하는 문장은 Query, 열에 해당하는 문장은 Key
Key에 <PAD>가 있는경우 해다아 열 전체를 마스킹 합니다.
마스킹을 하는 방법은 어텐션 스코어 행렬의 마스킹 위치에 매우 작은 음수값을 넣는 것인데,
그렇게 되면 소프트맥스 함수를 거쳐서 0이 되어 <PAD> 토큰이 반영되지 않게 됩니다.
패딩 마스크를 구현하는 방법은 입력된 정수 시퀀스에서 패딩 토큰의 인덱스인지,아닌지를 판별하는 함수를 구현하면 됩니다.
def create_padding_mask(x):
# 값이 0이라면, 1을 반환 0이 아니라면 0을반환
mask = tf.cast(tf.math.equal(x, 0), tf.float32)
# mask = [[0,0,0,1,1]]
# (batch_size, 1, 1, key의 문장 길이)
# tf.newaxis 를 통해[1,1,1,5]로 reshape
return mask[:, tf.newaxis, tf.newaxis, :]
다음으로는 인코더의 또다른 서브층인 포지션-와이즈 피드 포워드 신경망(Position-wise FFNN)에 대해서 알아보겠습니다.
Position-wise FFNN
현재까지 인코더에 대해서 설명하고 있지만 , FFNN은 인코더와 디코더에서 공통적으로 가지고 있는 서브층입니다.포지션 와이즈 FFNN은 완전 연결 FFNN(Fully-Connected FFNN)으로 해석할 수 있습니다.$$ FFNN(x) = MAX(0, x{W_{1}} + b_{1}){W_2} + b_2 $$
- $x$ : 멀티 헤드 어텐션의 결과로 나온 (seq_len, $d_{model}$)의 크기를 가지는 행렬
- $W_1$ : ($d_{model}, d_{ff}$)의 크기를 갖는 가중치
- $W_2$ : ($d_{ff},d_{model}$)의 크기를 갖는 가중치
- $d_{ff}$는 논문에서 언급한듯이 2048의 크기를 갖음
여기서 매개변수 $W_1,b_1,W_2,b_2$는 하나의 인코더 층 내에서는 다른 문장, 다른 단어들마다 정확하게 동일하게 사용
하지만, 인코더 층마다는 다른 값을 가집니다.
잠깐 정리해보자면,
- 인코더 #1의 입력이 임베딩 벡터와 포지셔닝 인코딩을 통해 입력으로 들어가서,
- 멀티 헤드 셀프 언텐션층을 통해 각 Q,K,V 벡터를 만들고 이를 하나로 연결(seq_len,$d_{model})
- 그 후 FFNN을 통과 하고, 인코더 #1의 출력이 다음층 인코더의 입력으로 들어감
으로 정리할 수 있을 거 같습니다.
잔차 연결(Residual Connetion)과 층 정규화(Layer Normalization)
앞서 인코더의 서브층 2개에 대해서 알아봤습니다. 트랜스포머 구조를 살펴보면 두개의 서브층을 가진 인코더에 추가적응로 사용하는 기법이 있었는데, Add & Norm 입니다.정확히는 잔차 연결(residual connection)과 층 정규화(layer Normalization) 입니다.
각 서브층에서 실선의 화살표가 되어있는데, 이는 잔차연결과 층 정규화 이후에 이해해보도록 하겠습니다.
1) 잔차 연결
잔차 연결에 대해서 설명할때 자주 등장하는 그림입니다.
이 그림에서 $H(x)$ 에 대해서 잠시 알아보겠습니다.
위 그림은 입력 x와 x에 대한 어떤 함수 F(x)의 값을 더한 H(x) 구조로 볼수있는데요,
여기서! 어떤 함수 $F(x)$가 트랜스포머의 서브층에 해당됩니다(멀티 헤드 어텐션, FFNN 층 말합니다)
잔차연결이란 서브층의 입력과 출력을 더하는 것을 말합니다.
앞서 트랜스포머에서 서브층의 입력과 출력은 동일한 차원이므로, 덧셈연산이 가능합니다.
잔차연결은 컴퓨터 비전 분야에서 주로 사용되는 분야로 모델의 학습을 돕는데,
식을 표현하면, $x+Sublayer(x)$ 로 나타낼 수 있습니다.
잔차연결(서브층이 멀티헤드 어텐션)
$ H(x) = x+Multi head\ Attention(x) $
2) 층 정규화
잔차연결을 거친 결과는 이어서 층 정규화를 거칩니다.
잔차연결의 입력을 x , 잔차연결과 층 정규화 두 가지 연산을 모두 수행한 후의 결과 행렬을 LN 이라하면,
$$ LN = LayerNorm(x+Sublayer(x)) $$
층 정규화는 텐서의 마지막 차원에 대해서 평균과 분산을 구하고, 이를 가지고 어떤 수식을 통해서 정규화하여 학습을 돕습니다. 여기서 텐서의 마지막 차원은 $d_{model}$ 차원을 의미합니다.
$d_{model}$ 차원의 방향을 화살표로 살펴보면,
(col 방향이므로)
각 화살표 방향의 벡터를 $x_i$라고 명명해보겠습니다.
층 정규화를 위해 화살표 방향으로 평균,분산을 구한뒤,
층 정규화를 수행한 후 벡터 $x_i$는 $ln_i$라는 벡터로 정규화가 됩니다.
$$ln_{i} = LayerNorm(x_{i})$$
층 정규화 수식
층 정규화 방식에는 2가지가 존재합니다.
첫번째는 평균과 분산 방식,
두번째는 감마와 베타를 도입 하는 방식이 있습니다.
우선, 평균과 분산 방식으로 정규화를 살펴보면 $x_i$를 정규화 합니다.
근데 $x_i$는 벡터고 , 평균과 분산은 스칼라 라는 값이기 때문에 벡터 $x_i$의 각 차원을 k 라했을때 $x_{i,k}$는 다음과 같이 정규화를 시행합니다.
$$\hat{x}_{i, k} = \frac{x_{i, k}-μ_{i}}{\sqrt{σ^{2}_{i}+\epsilon}}$$
두번째 방식 감마$γ$와 베타 방식$β$을 살펴보면, 이 둘의 초기값은 1과 0으로 시작합니다.
γ와 β를 이용한 층 정규화식은 다음과 같습니다.
$$ln_{i} = γ\hat{x}_{i}+β = LayerNorm(x_{i})$$
인코더 구현하기
def encoder_Layer(dff, d_model, num_heads, dropout, name="encoder_layer"):
inputs = tf.keras.Input(shape=(None,d_model),name="inputs")
# Padding Mask
padding_mask = tf.keras.Input(shape=(1,1,None),name="padding_mask)
# Multi-head Attention (First SubLayer / Self Attention)
attention = MultilHeadAttention(
d_model,num_heads,name="attention)({
'query':inputs, 'key':inputs, 'value':inputs, 'mask':padding_mask})
# Dropout, Residual Connection, Layer Normalization
attention = tf.keras.layers.Dropout(rate=dropout)(attention)
attention = tf.keras.layers.LayerNormalization(epsilon=1e-6)(inputs + attention)
# Position-wise FFNN
outputs = tf.keras.layers.Dense(units=dff, activation="relu")(attention)
outputs = tf.keras.layers.Dense(units=d_model)(outputs)
# Dropout + Residual Connection and Layer Normalization
outputs = tf.keras.layers.Dropout(rate=dropout)(outputs)
outputs= tf.keras.layers.LayerNormalization(
epsilon=1e-6)(attention + outputs)
return tf.keras.Model(
inputs=[inputs,padding_mask],outputs=outputs,name=name)
위의 코드의 경우 하나의 인코더 층을 구현한 것입니다. 트랜스포머의 경우 해당 층이 num_layers 개수만큼 인코더 층을 사용하므로 여러번 쌓아야합니다.
def encoder(vocab_size, num_layers, dff,
d_model, num_heads, dropout,
name="encoder"):
inputs = tf.keras.Input(shape=(None,), name="inputs")
# 인코더는 패딩 마스크 사용
padding_mask = tf.keras.Input(shape=(1, 1, None), name="padding_mask")
# 포지셔널 인코딩 + 드롭아웃
embeddings = tf.keras.layers.Embedding(vocab_size, d_model)(inputs)
embeddings *= tf.math.sqrt(tf.cast(d_model, tf.float32))
embeddings = PositionalEncoding(vocab_size, d_model)(embeddings)
outputs = tf.keras.layers.Dropout(rate=dropout)(embeddings)
# 인코더를 num_layers개 쌓기
for i in range(num_layers):
outputs = encoder_layer(dff=dff, d_model=d_model, num_heads=num_heads,
dropout=dropout, name="encoder_layer_{}".format(i),
)([outputs, padding_mask])
return tf.keras.Model(
inputs=[inputs, padding_mask], outputs=outputs, name=name)
다음 글에서는 인코더에서 디코더로 출력을 전달하는 것과 디코더에 대해서 정리하겠습니다.
관련글
'# AI 이론 > DeepLearning' 카테고리의 다른 글
Variational AutoEncoder(VAE) (0) | 2022.05.12 |
---|---|
트랜스포머 정리(3) (0) | 2022.05.10 |
트랜스포머 정리(1) (0) | 2022.05.09 |
어텐션 메커니즘 (0) | 2022.05.09 |
RNN,LSTM,GRU에 대해서 알아보자 (0) | 2022.05.03 |