Transformer Model [트랜스포머 모델] 정리 - [2]
이 글은 Transformer 에 대해 직관적으로 이해하고 이해한 바를 잊지 않기 위해 여러 글을 참고하여 작성 / 정리해둔 글입니다.
1. 인코더의 셀프 어텐션
트랜스포머는 하이퍼파라미터인 num_layers 의 수만큼 인코더 층을 쌓는다. 논문에서는 6개의 인코더 층을 사용했다. 인코더를 하나의 층이라는 개념으로 생각한다면, 인코더 한 개의 층은 셀프 어텐션과 피드 포워드 신경망 총 2개의 서브층으로 나눠진다.
위의 그림에서
멀티 헤드 셀프 어텐션 = 셀프 어텐션을 병렬적으로 사용
포지션 와이즈 피드 포워드 신경망 = 피드 포워드 신경망 을 뜻한다.
2. 포지션-와이즈 피드 포워드 신경망 [Position-wise FFNN]
1) 셀프 어텐션
어텐션 함수 :
1. 주어진 쿼리에 대해 모든 키와 유사도를 각각 구한뒤,
2. 그 유사도를 가중치로 하여 키와 맵핑되어 있는 각각의 값에 반영해준다.
3. 그리고 유사도가 반영된 값을 모두 가중합하여 리턴한다.
셀프 어텐션은 어텐션을 자기 자신에게 수행한다는 의미이다.
어텐션은 K와 V 가 인코더에서 나오지만 Q 는 디코더에서 나오기 때문에 Q와 K가 서로 다른 값을 가지고 있지만, 셀프 어텐션의 경우는 Q, K, V가 모두 동일하다.
셀프 어텐션에서의 Q, K, V 는 아래와 같다.
Q : 입력 문장의 모든 단어 벡터들
K : 입력 문장의 모든 단어 벡터들
V : 입력 문장의 모든 단어 벡터들
셀프 어텐션의 대표적인 효과는 다음과 같다.
위의 문장에서 중간에 나오는 it은 street 을 의미하는가, animal을 의미하는가 기계가 파악하기가 쉽지 않다. 하지만 셀프 어텐션에서는 입력 문장 내의 단어들끼리 유사도를 구하기 때문에 it 이 animal과 연관되어 있을 확률이 높다는 것을 찾아낼 수 있다.
2) Q, K, V 벡터 얻기
attention 에서 입력 임베딩을 통해 생성된다.
1. 입력 토큰 시퀀스는 임베딩 레이어를 통과하여 각 토큰이 임베딩 벡터로 변환된다. 이 때 임베딩은 $X$ 로 표현된다.
2. 벡터 $X$ 에 대해 선형 변환 [Linear Transformation] 을 적용한다.
$Q = X \dot W_{Q}$
$K = X \dot W_{K}$
$V = X \dot W_{V}$
이 때 $W_{Q}$, $W_{K}$, $W_{V}$ 는 각 Q, K, V 벡터로 변화하기 위해 학습 가능한 가중치 행렬이다.
3. Q, K 벡터를 이용해 어텐션 스코어를 계산하고, 이를 V 벡터에 적용하여 특정 위치의 정보가 다른 위치가 어떻게 연관되는지 확인 가능하다.
import torch
import torch.nn as nn
class SelfAttention(nn.Module):
def __init__(self, embed_size, heads):
super(SelfAttention, self).__init__()
self.embed_size = embed_size
self.heads = heads
self.head_dim = embed_size // heads
assert (
self.head_dim * heads == embed_size
), "Embedding size needs to be divisible by heads"
# Query, Key, Value에 대한 선형 변환 정의
self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)
self.fc_out = nn.Linear(heads * self.head_dim, embed_size)
def forward(self, x):
N, seq_length, embed_size = x.shape
head_dim = embed_size // self.heads
assert head_dim * self.heads == embed_size, "Embedding size is incorrect"
# Query, Key, Value 벡터 생성
queries = self.queries(x)
keys = self.keys(x)
values = self.values(x)
return queries, keys, values # Q, K, V 벡터 반환
위의 코드에서 "assert" 문은 조건을 검사하고, 그 조건이 참이 아닐 경우 에러(AssertionError)를 보여주는 디버깅 도구이다. 코드가 실핼되는 동안 특정 조건이 반드시 충족되어야 할 때 사용된다.
기본 적으로 아래와 같이 사용된다.
assert 조건, "조건이 거짓일 경우 출력할 메시지"
3. 잔차 연결 [Residual connection] 과 층 정규화 [Layer Normalization]
잔차 연결과 층 정규화는 학습 안정성을 높이고 성능을 향상시키기 위해 사용된다.
1) 잔차 연결 [Residual connection]
입력 값을 그대로 다음 레이어에 전달하는 경로를 추가해서 기울기 소실 문제 [Gradient Vanishing] 을 줄이고 학습을 안정화 하는 방법이다. 트랜스포머의 각 서브 레이어 [셀프 어텐션이나 피드 포워드 층] 에서 사용된다.
$output = Layer(x) + x$
이 때 $Layer(x)$는 현재 층[지금은 피드 포워드 층]의 연산 결과이며, $x$는 입력이다.
장점?
- 기울기 소실 방지
- 정보 전달 유지
- 훈련 속도 향상
2) 층 정규화 [Layer Normalization]
층 정규화는 입력 데이터가 각 층을 통과할 때 층 단위로 정규화하여 학습이 안정되도록 돕는 정규화 기법이다. 트랜스포머에서는 Batch Normalization 대신 층 정규화를 사용한다.
$LayerNorm(x) = \frac{x-\mu}{\sigma} \dot \gamma + \beta$
이 때 $\mu$ 는 입력 $x$ 의 평균, $\sigma$ 는 입력 $x$의 표준편차, $\gamma$ 와 $\beta$ 는 학습 가능한 파라미터로 정규화된 값의 스케일과 이동을 조정한다.
트랜스포머는 각 서브 레이어 [셀프 어텐션, 피드 포워드] 에서 잔차 연결과 층 정규화를 적용하는데
LayerNorm -> SubLayer -> Residual Connection 순서로 사용한다.
# 트랜스포머 층 정규화 및 잔차 연결 (Pytorch 버전)
import torch
import torch.nn as nn
class TransformerLayer(nn.Module):
def __init__(self, embed_size):
super(TransformerLayer, self).__init__()
self.layer_norm = nn.LayerNorm(embed_size)
self.self_attention = nn.MultiheadAttention(embed_size, num_heads=8)
self.feed_forward = nn.Sequential(
nn.Linear(embed_size, embed_size * 4),
nn.ReLU(),
nn.Linear(embed_size * 4, embed_size)
)
def forward(self, x):
# Self-Attention with LayerNorm and Residual Connection
attn_output, _ = self.self_attention(x, x, x)
x = self.layer_norm(x + attn_output) # 잔차 연결 및 층 정규화
# Feed Forward Network with LayerNorm and Residual Connection
feedforward_output = self.feed_forward(x)
x = self.layer_norm(x + feedforward_output) # 잔차 연결 및 층 정규화
return x
# numpy 버전
import numpy as np
def layer_norm(x, epsilon=1e-5):
# 각 샘플별 평균과 표준 편차 계산 (마지막 축 기준)
mean = np.mean(x, axis=-1, keepdims=True)
std = np.std(x, axis=-1, keepdims=True)
# 정규화 수행
x_norm = (x - mean) / (std + epsilon)
# 학습 가능한 파라미터 gamma와 beta 초기화 (여기서는 1과 0으로 설정)
gamma = np.ones_like(mean)
beta = np.zeros_like(mean)
# 스케일과 이동 적용
output = gamma * x_norm + beta
return output
# 예제 입력 데이터 (2D 배열)
# x = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
# 층 정규화 수행
# output = layer_norm(x)
4. 디코더의 첫 번째 서브층 : 셀프 어텐션과 룩-어헤드 마스크
트랜스포머 모델의 디코더는 주로 생성 작업에 사용되며 디코더의 첫 번째 서브층은 셀프 어텐션과 룩-어헤드 마스크로 구성되어 있다. 이 때 다음 단어를 예측할 때 미래 정보를 참조하지 않도록 제약을 걸어준다.
다음 단어를 예측할 때 미래 정보를 참조하지 않도록 제약을 걸어주는 이유는 언어 모델의 학습 및 생성 과정에서 자연스러운 시퀀스 생성을 위해서이다.
그로 인해 언어 모델의 순차적 특성을 유지하고 테스트 과정에서 일관성을 유지할 수 있으며, 실제 텍스트 생성에서 의미적 흐름을 가져갈 수 있다.
1) 디코더의 셀프 어텐션 [Self-Attention]
디코더가 현재 시점까지 생성된 토큰들을 참조하여 새로운 토큰을 생성할 수 있도록 해주는 메커니즘이다. 이 부분에서는 각 토큰이 자신을 포함한 이전의 토큰들과 상호작용하여 문맥 정보를 추출한다. 때문에 입력 시퀀스 뿐 아니라 지금까지 디코더에서 생성한 모든 단어를 고려하여 다음 단어를 예측할 수 있게 된다. [이전 단어 참조 및 문맥 정보 활용]
2) 디코더의 룩-어헤드 마스크 [Look-Ahead Mask]
미래의 단어가 어텐션에 포함되지 않도록 차단하는 마스크이다. 트랜스포머에서 디코더는 시퀀스를 한 번에 병렬로 처리하지만, ㅜ언어 모델에서는 현재 시점 이후의 단어를 참조할 수 없도록 차단해야 한다.
룩-어헤드 마스크는 하삼각 행렬 (lower triangular matrix) 로 구현된다. 현재 시점 이전의 위치에는 1을, 이후 위치에는 0을 할당하여 현재 시점 이후의 단어를 차단한다.
import numpy as np
def create_look_ahead_mask(size):
# 하삼각 행렬 생성
lower_triangular_matrix = np.tril(np.ones((size, size)))
return lower_triangular_matrix
# 시퀀스 길이가 5인 룩-어헤드 마스크 생성
look_ahead_mask = create_look_ahead_mask(5)
print(look_ahead_mask)
5. 디코더의 두 번째 서브층 : 인코더-디코더 어텐션
디코더가 인코더 출력을 참조하여 입력 시퀀스와 디코더에서 생성 중인 시퀀스 간의 연관성을 학습한다. 디코더의 첫 번째 서브층인 셀프 어텐션에서는 디코더 입력에만 의존하지만, 인코더-디코더 어텐션에서는 인코더에서 얻은 입력 시퀀스의 정보를 참조한다.
해당 층에서 Query 는 디코더의 첫 번째 서브층의 출력이 Key와 Value 는 인코더의 출력이 된다.
# numpy 버전
import numpy as np
# 소프트맥스 함수 정의
def softmax(x):
ex = np.exp(x - np.max(x))
return ex / ex.sum(axis=-1, keepdims=True)
# 인코더-디코더 어텐션 함수 정의
def encoder_decoder_attention(query, key, value):
# 1. 어텐션 스코어 계산 (Query와 Key의 내적) + 차원 맞추기 위해 전치행렬 사용
scores = np.dot(query, key.T) # (디코더 길이, 인코더 길이)
# 2. 소프트맥스를 통해 어텐션 가중치 계산
attention_weights = softmax(scores / np.sqrt(key.shape[-1]))
# 3. Value와 어텐션 가중치를 곱하여 최종 어텐션 출력 계산
output = np.dot(attention_weights, value)
return output, attention_weights
# 샘플 입력 데이터 정의 (차원 예시: 4차원 임베딩)
query = np.array([[0.1, 0.2, 0.3, 0.4]]) # 디코더에서 온 Query 벡터
key = np.array([[0.5, 0.6, 0.7, 0.8]]) # 인코더에서 온 Key 벡터
value = np.array([[0.9, 1.0, 1.1, 1.2]]) # 인코더에서 온 Value 벡터
# 인코더-디코더 어텐션 수행
output, attention_weights = encoder_decoder_attention(query, key, value)
# torch 버전
import torch
import torch.nn as nn
class TransformerDecoderLayer(nn.Module):
def __init__(self, embed_size, heads):
super(TransformerDecoderLayer, self).__init__()
self.self_attention = nn.MultiheadAttention(embed_size, heads)
self.enc_dec_attention = nn.MultiheadAttention(embed_size, heads)
self.feed_forward = nn.Sequential(
nn.Linear(embed_size, embed_size * 4),
nn.ReLU(),
nn.Linear(embed_size * 4, embed_size)
)
self.layer_norm1 = nn.LayerNorm(embed_size)
self.layer_norm2 = nn.LayerNorm(embed_size)
self.layer_norm3 = nn.LayerNorm(embed_size)
def forward(self, x, enc_output):
# 1. Self-Attention + Residual & LayerNorm
_x = x # 초기 x copy
x, _ = self.self_attention(x, x, x) # 셀프 어텐션
# 이 때 각 x가 Query, Key, Value 로 들어간다
# (새로운 x, 어텐션 가중치) 로 반환되는데, 어텐션 가중치는 무시
x = self.layer_norm1(x + _x) # 정규화
# 2. Encoder-Decoder Attention + Residual & LayerNorm
_x = x
x, _ = self.enc_dec_attention(x, enc_output, enc_output)
x = self.layer_norm2(x + _x)
# 3. Feed Forward + Residual & LayerNorm
_x = x
x = self.feed_forward(x)
x = self.layer_norm3(x + _x)
return x