본 프로젝트는 트랜스포머(Transformer) 아키텍처를 PyTorch로 직접 구현하고, 데이터 규모가 모델의 문맥 파악 능력과 일반화 성능에 미치는 영향을 분석하기 위해 두 차례의 실험을 진행하였습니다.
논문 링크: https://arxiv.org/abs/1706.03762
Attention Is All You Need
The dominant sequence transduction models are based on complex recurrent or convolutional neural networks in an encoder-decoder configuration. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new
arxiv.org
논문 정리 블로그 참고: 트랜스포머 논문 리뷰 블로그
Github 링크 참고: Attention-Is-All-You-Need-Review
1. 논문 구조 리뷰 및 구현 (Core Logic)
모델 아키텍처는 데이터의 양과 상관없이 논문의 핵심 구조를 모두 반영하여 클래스 단위로 구현되었습니다.
- Multi-Head Attention: $d_k$ 스케일링을 포함한 병렬 어텐션 로직 구현.
- Positional Encoding: 삼각함수를 이용한 위치 정보 주입.
- Encoder Layers: Residual Connection 및 Layer Normalization이 포함된 인코더 블록 설계.
2. 데이터셋 및 실험 설정 (Dataset & Setup)
- 데이터셋: IMDB Movie Reviews (이진 분류)
- Tokenizer:
bert-base-uncased(Max Length: 256) - 모델 설정: $d_{model}=128$, $num_heads=8$, $num_layers=2$, $batch_size=16$
3. 실험 결과 및 비교 분석 (Experimental Results)
학습 데이터의 규모에 따른 모델의 성능 변화를 아래와 같이 분석하였습니다.
[실험 1] 샘플링 데이터 학습 (Small Scale)
- 데이터 규모: Train 2,000 / Test 500
- 성능: 최종 Test Acc 74.20%
- 특징: 강한 긍정/부정 단어는 잘 포착하나, 복잡한 문장 구조에서 한계를 보임.
- 추론 한계: *"It was okay, but the ending was a bit disappointing."* 문장을 긍정(83.33%)으로 오분류함. (단어 'okay'에 과도하게 집중)
[실험 2] 전체 데이터 학습 (Full Scale)
- 데이터 규모: Train 25,000 / Test 25,000
- 성능: 최종 Test Acc 84.10% (약 10%p 향상)
- 특징: 데이터 양이 늘어남에 따라 문장 내 미묘한 뉘앙스와 역접 구조를 명확히 학습함.
- 추론 개선: 동일한 문장에 대해 부정(91.08%)으로 정확히 분류 성공.
| 실험 항목 | 실험 1 (Sampled) | 실험 2 (Full Dataset) |
|---|---|---|
| 학습 데이터 수 | 2,000개 | 25,000개 |
| 최종 Loss | 0.3814 | 0.2347 |
| Test Accuracy | 74.20% | 84.10% |
| 복합 문장 분류 | 오분류 (긍정 판단) | 정확 분류 (부정 판단) |
4. 최종 결과 해석
- 데이터 스케일의 중요성: 동일한 트랜스포머 로직 하에서도 데이터의 양이 늘어남에 따라 모델의 일반화 성능(Generalization)이 크게 향상됨을 확인했습니다.
- 문맥 파악 능력 향상: 실험 2에서 "but" 이후의 부정적인 뉘앙스를 정확히 읽어낸 것은, 충분한 데이터를 통해 Self-Attention 메커니즘이 문장 내 특정 단어('okay')에 매몰되지 않고 전체적인 맥락의 가중치를 올바르게 학습했음을 증명합니다.
- 결론: 본 프로젝트를 통해 직접 구현한 트랜스포머 모델이 대규모 데이터셋에서 실제 논문의 성능 의도와 부합하게 작동함을 확인하였습니다.
트랜스포머(Transformer) 확장 및 응용 분석
트랜스포머 아키텍처는 2017년 발표 이후 자연어 처리(NLP)를 넘어 컴퓨터 비전, 오디오 등 전 분야를 아우르는 범용 인공지능의 표준으로 자리 잡았습니다. 2026년 현재, 트랜스포머는 연산 효율성을 극대화한 하이브리드 구조로 진화하고 있습니다.
1. 트랜스포머를 응용한 대표적인 구조
트랜스포머는 인코더와 디코더의 활용 방식에 따라 크게 세 가지 방향으로 파생되었습니다.
BERT (Bidirectional Encoder Representations from Transformers)
- 구조: 트랜스포머의 인코더(Encoder) 블록만 활용합니다.
- 특징: 문장의 앞뒤 문맥을 동시에 파악하는 양방향(Bidirectional) 학습을 수행합니다.
- 주요 용도: 문장 분류, 감성 분석, 질의응답(Q&A) 등 문맥 이해 작업.
GPT (Generative Pre-trained Transformer)
- 구조: 트랜스포머의 디코더(Decoder) 블록만 활용합니다.
- 특징: 이전 단어들을 바탕으로 다음 단어를 예측하는 단방향(Autoregressive) 생성 방식을 사용합니다.
- 주요 용도: 대화형 AI, 창작, 코드 생성 등 텍스트 생성 작업.
ViT (Vision Transformer)
- 구조: 인코더 구조를 이미지 처리에 이식했습니다.
- 특징: 이미지를 격자 형태의 패치(Patch) 단위로 나누어 텍스트 토큰처럼 처리합니다.
- 주요 용도: 이미지 분류, 객체 탐지 등 컴퓨터 비전 작업.
2. 트랜스포머가 범용 아키텍처가 된 이유
- 데이터 통합 (Tokenization): 어떤 데이터든 '토큰' 단위로 변환하면 동일한 어텐션 메커니즘으로 처리 가능한 범용성을 가집니다.
- 강력한 병렬 연산: RNN과 달리 전체 데이터를 한 번에 처리하여 대규모 GPU 학습에 최적화되어 있습니다.
- 확장 법칙 (Scaling Law): 모델 크기와 데이터 양을 늘릴수록 성능이 지속적으로 향상되는 특성이 있어 거대 모델 구축에 유리합니다.
- 낮은 귀납적 편향 (Low Inductive Bias): 데이터의 구조적 제약 없이 관계를 스스로 학습하여 방대한 데이터에서 정교한 패턴을 찾아냅니다.
3. 트랜스포머 진화 과정 및 비교
대표 모델 비교 표
| 구분 | BERT | GPT | ViT |
|---|---|---|---|
| 중심 구조 | 인코더 (Encoder) | 디코더 (Decoder) | 인코더 (Encoder) |
| 학습 방식 | 양방향 문맥 이해 | 단방향 문장 생성 | 이미지 패치 관계 학습 |
| 입력 단위 | 텍스트 토큰 | 텍스트 토큰 | 이미지 패치 |
| 핵심 강점 | 정교한 의미 분석 | 자연스러운 문장 생성 | 시각적 특징 추출 |
트랜스포머 진화 흐름
- 2017 (탄생): Vanilla Transformer 등장 (Attention Is All You Need).
- 2018~2019 (분화): 이해 중심의 BERT와 생성 중심의 GPT 시리즈 시작.
- 2020 (확장): 이미지를 처리하는 ViT 등장으로 멀티모달 시대 개막.
- 2021~2024 (거대화): GPT-4, Llama 등 조 단위 파라미터 모델 및 효율적 연산 기법(FlashAttention) 도입.
- 2025~2026 (하이브리드):
- MoE (Mixture of Experts): 연산 효율을 높인 전문가 혼합 구조 (DeepSeek 등).
- SSM 결합: 긴 문맥 처리를 위해 Mamba 구조 등과 결합한 차세대 아키텍처 연구 활발.
전체 구현 코드
!pip -q install datasets transformers
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from torch.utils.data import DataLoader, TensorDataset
from datasets import load_dataset
from transformers import AutoTokenizer
from dataclasses import dataclass
from tqdm.auto import tqdm
dataset = load_dataset('imdb')
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
def tokenize(batch):
return tokenizer(batch['text'], padding='max_length', truncation=True, max_length=256)
train_data = dataset['train'].shuffle(seed=42)
test_data = dataset['test'].shuffle(seed=42)
train_tokenized = train_data.map(tokenize, batched=True)
test_tokenized = test_data.map(tokenize, batched=True)
def make_loader(data, batch_size=16):
ids = torch.tensor(data['input_ids'])
labels = torch.tensor(data['label'])
return DataLoader(TensorDataset(ids, labels), batch_size=batch_size, shuffle=True)
train_loader = make_loader(train_tokenized, batch_size=16)
test_loader = make_loader(test_tokenized, batch_size=16)
PositionalEncoding 구현
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=256, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# div_term = 1 / 10000^(2i/d_model)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
pe[:, 0::2] = torch.sin(position * div_term)
# PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
# x = x + PE: Embedding 벡터에 위치 정보 합산
return self.dropout(x + self.pe[:, :x.size(1)])
MultiHeadAttention 구현
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads, dropout=0.1):
super().__init__()
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.w_q = nn.Linear(d_model, d_model)
self.w_k = nn.Linear(d_model, d_model)
self.w_v = nn.Linear(d_model, d_model)
self.fc = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, q, k, v):
batch_size = q.size(0)
# Linear projection: Q, K, V 생성 (Query, Key, Value)
# Split into heads: (batch, seq_len, num_heads, d_k) -> (batch, num_heads, seq_len, d_k)
q = self.w_q(q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
k = self.w_k(k).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
v = self.w_v(v).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# Scaled Dot-Product Attention: Attention(Q, K, V) = softmax(QK^T / sqrt(d_k))V
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
attn = torch.softmax(scores, dim=-1)
# MultiHead(Q, K, V) = Concat(head_1, ..., head_h)W^O
context = torch.matmul(self.dropout(attn), v).transpose(1, 2).reshape(batch_size, -1, self.num_heads * self.d_k)
return self.fc(context)
TransformerEncoderLayer 구현
class TransformerEncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, dropout=0.1):
super().__init__()
self.mha = MultiHeadAttention(d_model, num_heads, dropout)
self.norm1 = nn.LayerNorm(d_model)
# FFN(x) = max(0, xW1 + b1)W2 + b2
self.ffn = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.ReLU(),
nn.Linear(d_model * 4, d_model)
)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# Sublayer 1: Residual Connection & Layer Normalization -> LayerNorm(x + Sublayer(x))
attn_out = self.mha(x, x, x)
x = self.norm1(x + self.dropout(attn_out))
# Sublayer 2: Position-wise Feed-Forward Network & LayerNorm
ffn_out = self.ffn(x)
x = self.norm2(x + self.dropout(ffn_out))
return x
TransformerSentimentModel 구현
class TransformerSentimentModel(nn.Module):
def __init__(self, vocab_size, d_model, num_heads, num_layers=2, num_classes=2, dropout=0.1):
super().__init__()
# Input Embedding: 문자를 d_model 차원의 벡터로 변환
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_encoding = PositionalEncoding(d_model, dropout=dropout)
# N x Encoder Layers: 논문의 Nx 구조 (중첩된 인코더 층)
self.layers = nn.ModuleList([TransformerEncoderLayer(d_model, num_heads, dropout) for _ in range(num_layers)])
# Final Linear Layer: 분류를 위한 출력층
self.classifier = nn.Linear(d_model, num_classes)
def forward(self, x):
x = self.embedding(x)
x = self.pos_encoding(x)
for layer in self.layers:
x = layer(x)
# Global Average Pooling: (batch, seq_len, d_model) -> (batch, d_model)
x = x.mean(dim=1)
return self.classifier(x)
학습 설정
class Trainer:
def __init__(self, model, config, train_loader, test_loader):
self.model = model.to(config.device)
self.config = config
self.train_loader = train_loader
self.test_loader = test_loader
self.optimizer = torch.optim.Adam(self.model.parameters(), lr=config.lr)
self.criterion = nn.CrossEntropyLoss()
def train(self):
for epoch in range(self.config.epochs):
self.model.train()
total_loss = 0
for batch in tqdm(self.train_loader, desc=f"Epoch {epoch+1}"):
ids, labels = [b.to(self.config.device) for b in batch]
self.optimizer.zero_grad()
loss = self.criterion(self.model(ids), labels)
loss.backward()
self.optimizer.step()
total_loss += loss.item()
acc = self.evaluate()
print(f"Loss: {total_loss/len(self.train_loader):.4f} | Test Acc: {acc:.2f}%")
def evaluate(self):
self.model.eval()
correct, total = 0, 0
with torch.no_grad():
for batch in self.test_loader:
ids, labels = [b.to(self.config.device) for b in batch]
out = self.model(ids)
correct += (out.argmax(1) == labels).sum().item()
total += labels.size(0)
return 100 * correct / total
def predict_sentiment(text, model, tokenizer, device):
model.eval()
with torch.no_grad():
inputs = tokenizer(text, return_tensors="pt", padding='max_length',
truncation=True, max_length=256).to(device)
output = model(inputs['input_ids'])
prob = torch.softmax(output, dim=-1)
pred = output.argmax(1).item()
label = "긍정 😊" if pred == 1 else "부정 😡"
print(f"리뷰: {text}\n결과: {label} ({prob[0][pred].item()*100:.2f}%)")
print("-" * 50)
모델 설정
@dataclass
class Config:
vocab_size: int = tokenizer.vocab_size
d_model: int = 128
num_heads: int = 8
epochs: int = 12
lr: float = 1e-4
device: torch.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
config = Config()
model = TransformerSentimentModel(config.vocab_size, config.d_model, config.num_heads)
학습 시작
trainer = Trainer(model, config, train_loader, test_loader)
trainer.train()
테스트 문구 생성
test_texts = [
"This movie was a masterpiece. The depth of the characters was incredible.",
"I hated this film. It was way too long and very boring.",
"It was okay, but the ending was a bit disappointing."
]
for text in test_texts:
predict_sentiment(text, model, tokenizer, config.device)