본문 바로가기

ML-DL/LLM

Transformer Model [트랜스포머 모델] 정리 - [3]

이 글은 Transformer 에 대해 직관적으로 이해하고 이해한 바를 잊지 않기 위해 여러 글을 참고하여 작성 / 정리해둔 글입니다.

 

 

1. Transformer 의 주요 하이퍼파라미터

1) 입력과 출력의 크기 [= 임베딩 크기, Embedding Size]

  • 모델이 학습할 단어 표현의 차원을 결정한다.
  • 임베딩 크기가 클수록 더 넓은 표현이 가능하지만 모델 복잡도와 메모리 사용량도 함께 증가한다.

2) 인코더와 디코더의 층 [= 레이어 수, Num of Layers]

  • 층이 많을수록 모델이 복잡한 패턴을 학습할 수 있지만 과적합이 발생할 수 있다.
  • BERT 나 GPT 모델에서는 12, 24, 48 레이어를 사용하는 경우가 많다.

3) 어텐션 헤드의 수 [= Num of Attention Heads]

  • 멀티헤드 어테션 ㅅ레이어에서 병렬로 처리되는 헤드 수를 의미한다.
  • 수가 클수록 다양한 정보에 대해 동시에 학습할 수 있지만 계산 비용이 높아진다.

4) 은닉층의 수 [= 은닉층 크기, Hidden Size]

  • 피드포워드 신경망의 은닉층의 수를 설정하는 파라미터로 피드포워드 레이어의 크기를 말한다.
  • 보통 임베딩 크기의 4배 정도로 설정된다.

5) 시퀀스 길이 [= Sequence Length]

  • 모델이 한 번에 처리할 수 있는 최대 토큰 수이다.
  • 트랜스포머의 경우 고정된 입력 길이를 사용하기 때문에 이 길이가 중요하다.

그 외에도 lr, batch size 등이 있다.

 

2.  포지셔널 인코딩 구현

포지셔널 인코딩의 계산 식은 다음과 같다.

 

$PE_{pos, 2i} = sin(\frac{pos}{10000^{\frac{2i}{d}}})$

 

$PE_{pos, 2i+1} = cos(\frac{pos}{10000^{\frac{2i}{d}}})$

 

이 때, pos 는 단어의 위치, $i$ 는 차원의 인덱스, $d$ 는 임베딩 차원을 의미한다.

 

import numpy as np

def positional_encoding(seq_len, d_model):

    # 포지셔널 인코딩 배열 초기화
    pe = np.zeros((seq_len, d_model))  # (시퀀스 길이, 임베딩 차원)
    
    # 각 위치와 임베딩 차원에 대해 계산
    for pos in range(seq_len):
        for i in range(0, d_model, 2):
            pe[pos, i] = np.sin(pos / (10000 ** ((2 * i) / d_model)))
            pe[pos, i + 1] = np.cos(pos / (10000 ** ((2 * i) / d_model)))
    
    return pe

 

3.  스케일드 닷-프로덕트 어텐션 구현

스케일드 닷-프로덕트 어텐션은 다음과 같은 순서를 가진다.

 

어텐션 스코어 계산 [Query 와 Key 의 내적] -> 스케일링 [$\sqrt{d_{k}}$ 로 나누어 스케일링 진행함으로 안정성 높임, $d_{k}$ 는 key 벡터의 차원] -> 소프트맥스 적용 [확률 분포 = 어텐션 가중치 출력] -> 가중합 계산 [value * 어텐션 가중치]

 

$Attention(Q, K, V) = softmax(\frac{QK^{T}}{\sqrt{d_{k}}})V$

 

import numpy as np


def scaled_dot_product_attention(Q, K, V):

    """
    Q: Query matrix of shape (..., seq_len_q, d_k)
    K: Key matrix of shape (..., seq_len_k, d_k)
    V: Value matrix of shape (..., seq_len_v, d_v)
    """

	# 1. Query와 Key의 내적 수행
    d_k = Q.shape[-1]
    scores = np.matmul(Q, K.T) / np.sqrt(d_k)
    
    # 2. 소프트맥스 함수 적용
    attention_weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
    attention_weights /= np.sum(attention_weights, axis=-1, keepdims=True)
    
    # 3. 가중합 계산
    output = np.matmul(attention_weights, V)
    
    return output, attention_weights


# Query, Key, Value 예시
Q = np.array([[1.0, 0.0, 0.5]])
K = np.array([[0.5, 0.2, 0.3], [0.1, 1.0, 0.5], [0.3, 0.8, 0.7]])
V = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])

 

4.  멀티헤드 어텐션 구현

여러개의 독립적인 어텐션 헤드로 서로 다른 표현을 동시에 학습할 수 있게 해준다. 각 어텐션 헤드는 Query, Key, Value 를 각각 다른 가중치로 변환하여 병렬로 계산한 후 결과를 결합해서 모델의 성능을 높인다.

 

$MultiHead(Q, K, V) = Concat(head_{1}, ... , head_{h})W^{O} $

 

각 어텐션 헤드의 수식은 다음과 같다.

 

$head_{i} = Attention(QW_{i}^{Q}, KW_{i}^{K}, VW_{i}^{V})$

 

이 때, 각 $W_{i}^{Q} , W_{i}^{K}, W_{i}^{V}$ 는 Query, Key, Value 를 변환하기 위한 가중치 행렬을 의미한다.

 

import numpy as np


def scaled_dot_product_attention(Q, K, V):

    """
    Scaled Dot-Product Attention
    각 헤드의 어텐션 스코어를 계산하고, 소프트맥스를 적용하여 가중치를 구함
    """
    d_k = Q.shape[-1]
    scores = np.dot(Q, K.T) / np.sqrt(d_k)
    attention_weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
    attention_weights /= attention_weights.sum(axis=-1, keepdims=True)
    output = np.dot(attention_weights, V)
    
    return output


def multi_head_attention(Q, K, V, num_heads):

    """
    Multi-Head Attention implementation
    """
    d_model = Q.shape[-1]
    d_k = d_model // num_heads
    
    # 가중치 행렬 초기화
    W_Q = np.random.rand(num_heads, d_model, d_k)
    W_K = np.random.rand(num_heads, d_model, d_k)
    W_V = np.random.rand(num_heads, d_model, d_k)
    W_O = np.random.rand(num_heads * d_k, d_model)
    
    # 각 헤드에 대해 Query, Key, Value 변환
    heads = []
    for i in range(num_heads):
        Q_i = np.dot(Q, W_Q[i])
        K_i = np.dot(K, W_K[i])
        V_i = np.dot(V, W_V[i])
        head = scaled_dot_product_attention(Q_i, K_i, V_i)
        heads.append(head)
    
    # 각 헤드의 결과를 연결하여 최종 출력 생성
    concat_heads = np.concatenate(heads, axis=-1)
    output = np.dot(concat_heads, W_O)
    
    return output


# 인풋 예시
Q = np.array([[1.0, 0.0, 0.5]])
K = np.array([[0.5, 0.2, 0.3], [0.1, 1.0, 0.5], [0.3, 0.8, 0.7]])
V = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])

 

5.  인코더 구현

import numpy as np

class EncoderLayer:

    def __init__(self, embed_size, num_heads, hidden_size):
    
        self.embed_size = embed_size
        self.num_heads = num_heads
        self.hidden_size = hidden_size
        
        # 셀프 어텐션을 위한 가중치
        self.W_Q = np.random.rand(embed_size, embed_size)
        self.W_K = np.random.rand(embed_size, embed_size)
        self.W_V = np.random.rand(embed_size, embed_size)
        
        # 피드포워드 레이어 가중치
        self.W_1 = np.random.rand(embed_size, hidden_size)
        self.W_2 = np.random.rand(hidden_size, embed_size)

    def scaled_dot_product_attention(self, Q, K, V):
    
        d_k = Q.shape[-1]
        scores = np.dot(Q, K.T) / np.sqrt(d_k)
        attention_weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
        attention_weights /= np.sum(attention_weights, axis=-1, keepdims=True)
        
        return np.dot(attention_weights, V)

    def forward(self, x):
    
        # 1. 셀프 어텐션
        Q = np.dot(x, self.W_Q)
        K = np.dot(x, self.W_K)
        V = np.dot(x, self.W_V)
        attention = self.scaled_dot_product_attention(Q, K, V)
        
        # 잔차 연결 및 층 정규화
        x = x + attention
        x = (x - x.mean(axis=-1, keepdims=True)) / (x.std(axis=-1, keepdims=True) + 1e-5)
        
        # 2. 피드포워드 네트워크
        ff_output = np.dot(x, self.W_1)
        ff_output = np.maximum(0, ff_output)  # ReLU 활성화 함수
        ff_output = np.dot(ff_output, self.W_2)
        
        # 잔차 연결 및 층 정규화
        x = x + ff_output
        x = (x - x.mean(axis=-1, keepdims=True)) / (x.std(axis=-1, keepdims=True) + 1e-5)
        
        return x


class TransformerEncoder:

    def __init__(self, num_layers, embed_size, num_heads, hidden_size):
        self.layers = [EncoderLayer(embed_size, num_heads, hidden_size) for _ in range(num_layers)]
        
    def forward(self, x):
        # 인코더 레이어 순차 처리
        for layer in self.layers:
            x = layer.forward(x)
            
        return x



# 인코더 설정
num_layers = 6        # 인코더 레이어 개수
embed_size = 512      # 임베딩 크기
num_heads = 8         # 어텐션 헤드 개수
hidden_size = 2048    # 피드포워드 은닉 크기



# 인코더 초기화
encoder = TransformerEncoder(num_layers, embed_size, num_heads, hidden_size)

# 인풋 예시 (시퀀스 길이 10, 임베딩 크기 512)
x = np.random.rand(10, embed_size)

# 인코더 출력
output = encoder.forward(x)