Table of Contents

728x90

Transformers는 Attention is All You Need 논문에서 제안된 구조로, 기본적으로 인코더-디코더 구조를 가지고 있습니다. 아래는 Transformer의 주요 부분을 코드로 구현한 예시와 함께 설명입니다. 예시 코드는 PyTorch로 작성된 Transformer의 일부 구성 요소입니다.

 

1. tokenization

먼저 "안녕하세요 저는 chatGPT입니다." 라는 문장을 transformer 모델에 넣으려면 단어들을 tokenization해서 숫자로 바꿔주는 작업이 필요합니다. tokenization 살펴보기

 

[LLM] Tokenization, 문장을 숫자로 변환하는 과정

자연어 문장을 Transformer 모델에 입력하려면 먼저 문장을 숫자로 변환하는 과정이 필요합니다. 이 과정을 토크나이저(tokenizer)가 수행합니다. Transformer 모델은 텍스트 데이터를 처리할 수 없고, 숫

everydaysummerbreeze.tistory.com

아래와 같이 bert기반 모델 (여기서는 bert-base-multilingual-cased라는 가짜 모델 이름을 넣었습니다)을 불러와서 해당 문장을 tokenization시켜줍니다.

from transformers import BertTokenizer

# BERT 모델의 토크나이저 로드
tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased')

# 문장 입력
sentence = "안녕하세요 저는 ChatGPT입니다"

# 토큰화 및 정수 인코딩
tokens = tokenizer.encode(sentence, add_special_tokens=True)

# 결과 확인
print("토큰화된 결과:", tokenizer.convert_ids_to_tokens(tokens))
print("정수 인코딩된 결과:", tokens)

결과는 다음과 같이 나옵니다.

 

  • '[CLS]': 문장의 시작을 나타내는 특별한 토큰입니다.
  • '[SEP]': 문장의 끝을 나타내는 특별한 토큰입니다.
  • "안녕하세요"는 서브워드 단위로 분리되어 "안", "##녕", "##하세요"로 쪼개졌습니다.
  • "저는"도 "저", "##는"으로 쪼개졌습니다.
  • "ChatGPT"는 그대로 분리되지 않고 서브워드 단위로 처리됩니다.

 

토큰화된 결과: ['[CLS]', '안', '##녕', '##하세요', '저', '##는', 'chat', 'gp', '##t', '##입', '##니다', '[SEP]']
정수 인코딩된 결과: [101, 9521, 11927, 11102, 11096, 35806, 13856, 10502, 28933, 11102, 12092, 102]

이렇게 토큰화된 결과를 숫자로 변환한 후 Transformer 모델에 입력하면 됩니다.

 

2. 입력이 Query, Key, Value로 바뀌는 과정

Transformer 모델에서는 입력이 먼저 임베딩(embedding) 레이어를 통해 고차원 벡터로 변환된 후, Query, Key, Value로 바뀌는 과정을 거칩니다. 이 과정을 단계별로 설명하겠습니다.

1. 정수 인코딩 값(입력 시퀀스)

입력 시퀀스는 이미 [101, 9521, 11927, 11102, 11096, 35806, 13856, 10502, 28933, 11102, 12092, 102]로 정수 인코딩이 되어 있습니다. 이 값들은 토크나이저를 통해 만들어졌고, 각 숫자는 고유한 단어 또는 서브워드에 대응합니다.

2. 임베딩 레이어(Embedding Layer)

Transformer 모델은 입력된 정수 시퀀스를 고차원 벡터 공간으로 매핑하기 위해 임베딩 레이어를 사용합니다. 각 토큰은 고정된 크기의 임베딩 벡터로 변환됩니다. 예를 들어, 토큰 ID 101은 [0.1, 0.3, -0.4, ...] 같은 768차원 벡터로 변환될 수 있습니다.

import torch
import torch.nn as nn

# 임베딩 레이어 정의
embedding_layer = nn.Embedding(num_embeddings=30522, embedding_dim=768)  # BERT 모델 기준

# 입력 시퀀스 (토크나이저 결과인 정수 인코딩 값)
input_ids = torch.tensor([101, 9521, 11927, 11102, 11096, 35806, 13856, 10502, 28933, 11102, 12092, 102])

# 임베딩 수행
embedded_input = embedding_layer(input_ids)

print("임베딩된 벡터:", embedded_input.shape)
  • 입력 시퀀스: [101, 9521, 11927, 11102, 11096, 35806, 13856, 10502, 28933, 11102, 12092, 102]는 길이 12의 정수 배열입니다.
  • 임베딩 결과: 각 토큰은 768차원 벡터로 변환되어, (12, 768) 크기의 텐서가 됩니다. 즉, 각 토큰이 768차원 벡터로 표현됩니다.

3. Query, Key, Value 계산

Transformer에서 각 입력 벡터는 Query, Key, Value로 변환됩니다. 이를 위해 입력 임베딩에 세 개의 선형 변환(linear transformation)을 적용합니다. 각 변환은 Query, Key, Value에 대한 벡터를 생성합니다.

# Query, Key, Value를 생성하기 위한 선형 변환 정의
d_model = 768  # 임베딩 차원
query_linear = nn.Linear(d_model, d_model)  # Query 변환
key_linear = nn.Linear(d_model, d_model)    # Key 변환
value_linear = nn.Linear(d_model, d_model)  # Value 변환

# Query, Key, Value 생성
query = query_linear(embedded_input)  # (12, 768) -> (12, 768)
key = key_linear(embedded_input)      # (12, 768) -> (12, 768)
value = value_linear(embedded_input)  # (12, 768) -> (12, 768)

print("Query 벡터 크기:", query.shape)
print("Key 벡터 크기:", key.shape)
print("Value 벡터 크기:", value.shape)
  • 선형 변환(Linear Transformation): 입력 임베딩된 벡터(768차원)에 각각 Query, Key, Value로 변환하는 3개의 독립적인 선형 변환을 적용합니다. 이 변환들은 모델이 학습하는 가중치를 통해 이루어집니다. 왜 선형변환을 하는지는 여기서 다룹니다.
  • Query, Key, Value: 각 토큰은 Query, Key, Value에 대응하는 768차원 벡터로 변환됩니다. 결과적으로, Query, Key, Value 모두 (12, 768)의 크기를 가집니다. 즉, 각 입력 토큰에 대해 768차원짜리 Query, Key, Value 벡터가 생성된 것입니다.
 

Transformer 모델에서 선형 변환(linear transformation)을 사용하는 이유

Transformer 모델에서 선형 변환(linear transformation)을 사용하는 이유는 입력 임베딩을 Query, Key, Value로 변환하여 Self-Attention 메커니즘을 적용하기 위해서입니다. 그럼 왜 이 선형 변환이 필요한지, 그

everydaysummerbreeze.tistory.com

 

4. Self-Attention을 위한 Query, Key, Value

생성된 Query, Key, Value는 Self-Attention 메커니즘에서 사용됩니다. Attention은 각 Query와 다른 토큰의 Key 간의 유사도를 계산한 후, 그 유사도를 기반으로 Value를 조합합니다.

# Self-Attention 수행 (이전 코드 재사용)
attention_layer = ScaledDotProductAttention()
output, attn_weights = attention_layer(query.unsqueeze(0), key.unsqueeze(0), value.unsqueeze(0))

print("Attention Output Shape:", output.shape)  # (1, 12, 768)
print("Attention Weights Shape:", attn_weights.shape)  # (1, 12, 12)
  • Query, Key, Value를 Self-Attention에 입력: Self-Attention 메커니즘에서 각 Query와 Key의 내적을 통해 유사도를 계산하고, 이를 기반으로 Value를 가중합해 최종 출력을 만듭니다.
  • 결과: output은 Attention이 적용된 벡터이고, attn_weights는 각 토큰 간의 유사도를 나타내는 가중치입니다.

요약

  1. 입력 문장은 토크나이저를 통해 정수 시퀀스로 변환됩니다.
  2. 임베딩 레이어는 정수 시퀀스를 고차원 벡터로 변환합니다.
  3. Query, Key, Value는 임베딩 벡터에 선형 변환을 적용하여 생성됩니다.
  4. Self-Attention 메커니즘은 Query, Key, Value를 활용하여 중요한 정보를 추출합니다.

이 과정을 통해 자연어 문장은 Transformer 모델이 처리할 수 있는 형식인 벡터 공간에서 학습이 이루어지게 됩니다.

즉, 모델의 성능을 좌지우지하는 것은 가중치입니다. 얼마나 좋은 가중치를 학습했느지에 따라 모델의 성능이 결정됩니다. 

 

티스토리

좀 아는 블로거들의 유용한 이야기, 티스토리. 블로그, 포트폴리오, 웹사이트까지 티스토리에서 나를 표현해 보세요.

www.tistory.com

 

 

3. Self-Attention Mechanism

Self-Attention은 입력 문장 내의 각 단어가 다른 단어들과의 관계를 고려하여 벡터를 업데이트하는 메커니즘입니다. 이를 구현하는 과정은 주로 Query, Key, Value 행렬을 통해 이루어집니다. 이를 통해 입력 문장 내의 단어들이 서로 어떻게 연결되는지를 파악할 수 있습니다. Self-attention은 문맥에 따라 단어의 중요성을 동적으로 조정합니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class ScaledDotProductAttention(nn.Module):
    def forward(self, query, key, value, mask=None):
        d_k = query.size(-1)  # Query와 Key의 차원 수
        scores = torch.matmul(query, key.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))

        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        attention = F.softmax(scores, dim=-1)
        output = torch.matmul(attention, value)
        return output, attention
  • Query, Key, Value: Self-Attention에서는 입력 문장에서 각각의 단어 벡터가 Query, Key, Value 행렬로 변환됩니다.
  • Scores: Query와 Key의 내적을 통해 유사도를 계산하고, 이를 차원에 따라 스케일링하여 처리합니다.
  • Softmax: 유사도에 softmax를 적용해 각 단어가 얼마나 중요한지를 나타내는 가중치를 만듭니다.
  • Weighted sum: 가중치를 Value 벡터에 곱해 최종 출력을 얻습니다.

4. Multi-Head Attention

Self-Attention을 여러 번 병렬적으로 처리하여, 다양한 표현을 학습하는 것이 Multi-Head Attention입니다. 여러 개의 attention heads를 사용하여 다양한 관점에서 입력 데이터를 분석합니다. 이렇게 하면 모델이 더 풍부한 표현을 학습할 수 있습니다.

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.head_dim = d_model // num_heads

        assert self.head_dim * num_heads == d_model, "d_model must be divisible by num_heads"

        self.q_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.fc_out = nn.Linear(d_model, d_model)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)

        # Linear transformations
        Q = self.q_linear(query)
        K = self.k_linear(key)
        V = self.v_linear(value)

        # Split into num_heads
        Q = Q.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        K = K.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        V = V.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)

        # Scaled Dot-Product Attention
        attention, _ = ScaledDotProductAttention()(Q, K, V, mask)

        # Concatenate heads
        attention = attention.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)

        # Final linear layer
        output = self.fc_out(attention)
        return output
  • num_heads: 각 Attention 헤드를 여러 개 사용하여 모델이 다양한 패턴을 학습할 수 있게 합니다.
  • Linear transformation: Query, Key, Value에 선형 변환을 적용해 모델이 다양한 시점에서 데이터를 바라볼 수 있도록 합니다.
  • head_dim: 각 head의 차원 수를 결정하고, 각 head가 독립적으로 Attention을 수행하도록 합니다.

5. Feed-Forward Network

Self-Attention 레이어의 출력은 Feed-Forward Network(FNN)로 전달됩니다. 각 attention layer 뒤에는 feed-forward neural network가 있어, 이 네트워크는 각 단어의 표현을 독립적으로 변환합니다. 

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        x = self.dropout(F.relu(self.linear1(x)))
        x = self.linear2(x)
        return x
  • d_model: 입력 차원 (즉, 임베딩 차원).
  • d_ff: 중간 레이어의 크기, 일반적으로 입력보다 훨씬 큽니다.
  • Dropout: 과적합을 방지하기 위해 Dropout을 사용합니다.

6. Positional Encoding

Transformer는 순차적인 데이터를 처리하기 위한 순서 정보를 가지고 있지 않기 때문에, 각 단어의 위치 정보를 추가하는 방법으로 positional encoding을 사용합니다. 이를 통해 입력 데이터의 순서 정보를 모델이 이해할 수 있습니다.

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super().__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + self.pe[:, :x.size(1)]
        return x
  • Sinusoidal functions: Transformer에서는 각 단어의 위치를 임베딩에 추가하기 위해 sin과 cos 함수의 값을 사용합니다.
  • Positional information: 이 정보를 입력 벡터에 더하여 단어의 위치를 모델이 인식할 수 있게 합니다.

7. Transformer Block

위의 구성 요소들을 하나의 Transformer 블록으로 조합하여 사용합니다.

class TransformerBlock(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        self.attention = MultiHeadAttention(d_model, num_heads)
        self.ffn = FeedForward(d_model, d_ff, dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # Multi-Head Attention
        attn_output = self.attention(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))  # Residual connection and LayerNorm

        # Feed Forward Network
        ffn_output = self.ffn(x)
        x = self.norm2(x + self.dropout(ffn_output))  # Residual connection and LayerNorm

        return x
  • Residual connections: 입력과 출력의 잔차 연결(residual connections)을 사용하여 정보 손실을 방지하고 깊은 네트워크를 안정적으로 훈련할 수 있도록 학습을 안정화합니다. 
  • Layer Normalization: 각 레이어 뒤에 정규화를 적용하여 훈련이 더 빠르고 안정적으로 진행되도록 합니다.

이 Transformer 블록을 여러 층 쌓으면 완전한 Transformer 모델을 만들 수 있습니다. Inference 및 학습 과정에서 중요한 구조는 디코더도 있지만, 위 구조는 기본적인 인코더의 예시입니다.

8.전체 구조

Transformer는 이와 같은 블록을 여러 번 쌓아서 입력 데이터를 점진적으로 변환하며, 자연어 처리 작업에서 매우 강력한 성능을 발휘합니다.