
PyTorch로 Transformer 모델 처음부터 구축하기 | Sparsh Sharma
요약
PyTorch를 사용하여 Transformer 아키텍처를 밑바닥부터 직접 구현하는 과정을 다룹니다. 멀티 헤드 어텐션, 포지셔널 인코딩, 학습 루프의 원리를 상세히 설명하며 모델의 내부 작동 방식을 이해하는 데 중점을 둡니다.
핵심 포인트
- 멀티 헤드 어텐션 및 포지셔널 인코딩의 구현 원리 학습
- RNN의 한계인 기울기 소실과 순차적 병목 현상 이해
- PyTorch를 활용한 실제 학습 가능한 아키텍처 구축 방법
- 모델을 블랙박스가 아닌 구조적 관점에서 이해하는 중요성
PyTorch를 사용하여 Transformer 모델을 처음부터 구축하는 전체 과정 — 멀티 헤드 어텐션 (multi-head attention), 포지셔널 인코딩 (positional encoding), 학습 루프 (training loops), 그리고 로컬 하드웨어 및 클라우드 GPU에서 학습하며 얻은 교훈을 다룹니다.
PyTorch로 Transformer 모델 처음부터 구축하기
저는 HuggingFace나 지름길 없이, 밑바닥부터 저만의 Transformer 모델을 학습시켰습니다. 여기 그 전체적인 분석 내용이 있습니다: 멀티 헤드 어텐션 (multi-head attention), 포지셔널 인코딩 (positional encoding), 학습 루프 (training loops), 그리고 이 기술들이 실제로 어떻게 작동하는지 깨닫게 해준 실수들입니다.

2026년 7월 28일 18분 읽기
왜 처음부터 구축하는가?
Transformer를 _사용하는 것_과 이해하는 것 사이에는 거대한 격차가 존재합니다. 저는 Manshverse를 위해 Groq, Gemini, Mistral과 같은 API를 통해 사전 학습된 모델 (pre-trained models)을 미세 조정 (fine-tuning)하는 데 수개월을 보냈습니다. 하지만 저는 계속해서 벽에 부딪혔습니다. 무언가 고장 났을 때, 왜 그런 일이 발생하는지 추론할 수 없었습니다. 저는 모델을 블랙박스 (black box)로 취급하고 있었습니다.
그래서 저는 처음부터 하나를 구축하기로 결심했습니다. 튜토리얼에서 본 장난감 수준의 구현이 아닙니다. 순수 PyTorch로 작성되어, 제 데이터를 통해 학습시키고 손실 곡선 (loss curves)을 연구할 수 있는 실제 학습 가능한 Transformer 아키텍처 (architecture)입니다. 목표는 GPT와 경쟁하는 것이 아니라, 이 기계를 이해하는 것이었습니다.
만약 당신이 그것을 처음부터 구축할 수 없다면, 당신은 그것을 진정으로 이해하고 있는 것이 아니다. — Richard Feynman (의역)
RNN으로 시작하기
Transformer를 다루기 전에, 저는 순환 신경망 (RNNs, Recurrent Neural Networks)부터 시작했습니다. 이는 의도적인 선택이었습니다. **순차적 처리 (sequential processing)**의 고통을 느껴보기 전까지는 **어텐션 (attention)**의 가치를 제대로 체감할 수 없기 때문입니다.
RNN은 시퀀스를 한 번에 하나의 토큰 (token)씩 처리합니다. 토큰 _t_의 은닉 상태 (hidden state)가 토큰 _t+1_로 전달됩니다. 이는 이론적으로는 우아하지만, 실제로는 재앙에 가깝습니다:
- 기울기 소실 (Vanishing gradients): 시퀀스가 길어짐에 따라 초기 토큰의 정보가 기하급수적으로 감소합니다. 50번째 토큰에 도달하면 모델은 1번째 토큰에 대한 기억을 거의 유지하지 못합니다.
- 순차적 병목 현상 (Sequential bottleneck): 훈련을 병렬화할 수 없습니다. 각 토큰은 이전 토큰에 의존합니다. 제 로컬 머신에서는 이로 인해 훈련 속도가 고통스러울 정도로 느려졌습니다.
- 장기 의존성 (Long-range dependencies): 모델은 약 20개 이상의 위치 차이가 나는 토큰 간의 관계를 학습할 수 없었습니다.
저는 셰익스피어 텍스트를 사용하여 문자 수준 (character-level) RNN을 훈련했습니다. 모델은 수백 에포크 (epochs) 이내에 기본적인 단어 구조를 학습했지만, 출력물은 개별 구절을 넘어설 경우 앞뒤가 맞지 않았습니다. 손실 (loss) 값은 1.8 부근에서 정체되어 더 이상 움직이지 않았습니다. 대부분의 사람들이 이 지점에서 포기하고 라이브러리로 넘어가는 반면, 저는 어텐션 (attention)으로 넘어갔습니다.
셀프 어텐션 (Self-Attention): 핵심 메커니즘
트랜스포머 (transformer) 논문("Attention Is All You Need")의 핵심 통찰은 순환 (recurrence) 구조를 어텐션 (attention)으로 완전히 대체하는 것입니다. 토큰을 순차적으로 처리하는 대신, 모든 토큰이 다른 모든 토큰을 병렬적으로 참조 (attend)할 수 있습니다.
수식은 다음과 같습니다. 벡터로 임베딩된 토큰 시퀀스에 대해, 세 가지 투영 (projections)을 계산합니다:
# Q, K, V 투영
Q = X @ self.W_q # 나는 무엇을 찾고 있는가?
K = X @ self.W_k # 나는 무엇을 포함하고 있는가?
...
Q @ K.transpose() 연산은 모든 토큰 쌍 사이의 호환성 점수 (compatibility score)를 계산합니다. 소프트맥스 (softmax)는 이를 확률로 정규화합니다. 그런 다음 attn_weights @ V는 모든 토큰 표현의 가중치 결합 (weighted combination)을 생성합니다.
√d_k로 나누는 과정은 매우 중요하며, 저는 처음에 이를 잊어버렸습니다. 스케일링 (scaling)이 없으면 차원이 커질수록 내적 (dot product) 값이 매우 커지게 되어, 소프트맥스 (softmax)를 기울기 (gradient)가 0에 가까운 영역으로 밀어넣게 됩니다. 이 코드 한 줄을 추가하기 전까지 제 모델은 전혀 수렴하지 않았습니다.
핵심 통찰
Self-attention은 시퀀스 길이(sequence length)에 대해 $O(n^2)$의 복잡도(complexity)를 가집니다. 즉, 모든 토큰이 다른 모든 토큰을 참조합니다. 이것이 Transformer의 컨텍스트 윈도우(context window)에 엄격한 제한이 있는 이유입니다. 또한 이것이 Transformer가 강력한 이유이기도 합니다. 단 한 번의 패스(pass)로 전체 시퀀스에 걸친 관계를 모델링할 수 있기 때문입니다.
Multi-Head Attention
단일 어텐션 헤드(attention head)는 한 가지 유형의 관계만 학습할 수 있습니다. Multi-head attention은 여러 개의 어텐션 헤드를 병렬로 실행하며, 각 헤드는 고유하게 학습된 투영(projection)을 가집니다. 그 후 결과들을 연결(concatenate)하고 다시 투영합니다.
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
...
저는 d_model=512와 함께 8개의 헤드를 사용하였으며, 이에 따라 각 헤드의 차원(dimension)은 64가 됩니다. 실제로 서로 다른 헤드들은 각기 다른 패턴을 참조하도록 학습됩니다. 어떤 헤드는 인접한 토큰(국소적 구문, local syntax)에 집중하고, 다른 헤드는 멀리 떨어진 의존 관계(의미적 관계, semantic relationships)에 집중합니다.
Positional Encoding
Attention은 순열 불변(permutation-invariant)적입니다. 즉, 토큰의 _순서(order)_를 알지 못합니다. 위치 정보가 없다면, 모델의 관점에서 "the cat sat on the mat"라는 문장은 "mat the on sat cat the"와 동일합니다.
원본 논문에서는 사인 함수 기반의 위치 인코딩(sinusoidal positional encodings)을 사용합니다.
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
...
사인 함수 인코딩은 영리한 방식입니다. 각 차원이 서로 다른 주파수로 진동하며, 각 위치에 대해 고유한 위치 "지문(fingerprint)"을 생성합니다. 또한 sin(a+b)를 sin(a)와 cos(a)의 선형 결합(linear combination)으로 표현할 수 있기 때문에, 모델은 선형 변환(linear transformations)을 통해 상대적 위치를 학습할 수 있습니다.
Full Architecture
전체 Transformer 블록(block)은 Multi-head attention을 피드 포워드 네트워크(feed-forward network), 레이어 정규화(layer normalization), 그리고 잔차 연결(residual connections)과 함께 쌓아 올린 구조입니다.
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
...
저는 (원문 논문의 방식인) post-norm (sublayer 이후의 LayerNorm) 대신 pre-norm (sublayer 이전의 LayerNorm)을 사용했습니다. Pre-norm은 학습 중에 훨씬 더 안정적입니다. 잔차 연결 (residual connections)을 통해 그래디언트 (gradients)가 더 고르게 흐르며, 초기 발산 (divergence)을 방지하기 위한 학습률 웜업 (learning rate warmup)이 필요하지 않았습니다.
최종 모델 구성: 6개 레이어, 8개 헤드, d_model=512, d_ff=2048. 파라미터 수는 약 4,500만 개 (45M)입니다.
학습 루프 (Training Loop)
Transformer를 학습시키는 과정은 이론이 연산량 (compute)이라는 냉혹한 현실과 만나는 지점입니다. 저의 첫 시도는 GPU가 없는 로컬 머신에서의 순수 CPU 학습이었습니다. 제 데이터셋으로 단 1 에포크 (epoch)를 수행하는 데 40분 이상이 걸렸습니다.
학습을 성공적으로 이끈 주요 결정 사항들:
- AdamW optimizer: 가중치 감쇠 (weight decay) 0.01을 적용했습니다. 일반적인 Adam은 몇 에포크 이내에 과적합 (overfitting)을 유발했습니다.
- Cosine annealing 학습률 스케줄: 3e-4에서 시작했습니다. 웜업 (warmup) 단계가 매우 중요했습니다. 피크 학습률 (peak LR)로 바로 뛰어오르면 즉시 손실 (loss) 발산이 발생했습니다.
- Gradient clipping: max_norm=1.0을 적용했습니다. 이것이 없었다면 가끔 발생하는 그래디언트 폭주 (gradient explosions)로 인해 50~100 스텝마다 NaN 손실이 발생했을 것입니다.
- Mixed precision training (fp16): GPU로 옮긴 후 적용했습니다. 품질 저하 없이 속도가 거의 2배 향상되었습니다.
디버깅 팁
만약 Transformer의 손실 (loss)이 조기에 정체되어 내려오지 않는다면, 어텐션 마스크 (attention mask)를 확인하세요. 저는
약 50 에포크(epochs) 동안 세심한 하이퍼파라미터 튜닝을 거친 끝에, 제 텍스트 데이터셋에서 0.85의 학습 손실(training loss)을 달성했습니다. 생성된 출력물은 문장 수준에서 일관성이 있었고 구문과 기본적인 의미론(semantics)에 대한 명확한 이해를 보여주었습니다. 이는 제가 시도했던 RNN보다 훨씬 뛰어난 결과였습니다.
제가 배운 것들
트랜스포머를 처음부터 구축하는 과정은 어떤 튜토리얼이나 API 래퍼로는 절대 배울 수 없는 것들을 가르쳐 주었습니다:
- 어텐션(Attention)은 마법이 아닙니다. 그것은 학습된 가중 평균입니다. '마법'은 수백만 번의 기울기 업데이트(gradient updates)를 거치면서 무엇을 평균 낼지, 그리고 각 토큰에 얼마나 많은 가중치를 부여할지를 학습하는 과정에서 나옵니다.
- 규모(Scale)가 전부입니다. 제 45M 파라미터 모델도 일관성 있는 텍스트를 생성할 수 있었습니다. GPT-3는 175B의 파라미터를 가지고 있습니다. 아키텍처 자체는 같으며, 차이는 순전히 규모, 데이터, 그리고 컴퓨팅 능력에 있습니다.
- ML 디버깅은 근본적으로 다릅니다. 웹 개발에서 버그는 잘못된 출력을 만듭니다. ML에서는 버그가 '약간 덜 정확한' 출력을 만들며, 그것이 버그인지 아니면 단순히 학습이 부족해서 그런 것인지 알기 어렵습니다. 이 둘을 구별하는 법을 배우는 것이 하나의 기술입니다.
- 손실 곡선(loss curve)이 모든 것을 말해줍니다. 급격한 스파이크는 기울기 폭발(gradient explosions)을 의미합니다. 초기 하락 이후의 평탄화 구간은 학습률(learning rate)이 너무 낮거나 아키텍처 병목 현상(architectural bottleneck)을 의미할 수 있습니다. 진동(Oscillation)은 배치 크기(batch size)가 너무 작다는 것을 뜻합니다. 저는 의사가 심전도(ECG)를 읽는 것처럼 손실 곡선을 읽는 법을 배웠습니다.
- 머신에 대한 이해는 사용 방식을 바꿉니다. 저만의 트랜스포머를 구축한 후, 저는 프롬프트 엔지니어링(prompt engineering), 파인튜닝(fine-tuning), 그리고 프로덕션 AI 시스템 디버깅 능력이 극적으로 향상되었습니다. Manshverse의 Groq 통합이 예상치 못한 방식으로 작동할 때도, 이제는 원리(first principles)로부터 '왜' 그런지 추론할 수 있게 되었습니다.
만약 당신이 AI 엔지니어링에 진심이라면, 트랜스포머를 처음부터 구축하는 것이 할 수 있는 가장 가치 있는 학습 경험이라고 주장하고 싶습니다. OpenAI보다 더 나은 모델을 만들게 될 것이기 때문이 아니라—오히려 기계를 이해하는 것이 당신을 더 나은 빌더로 만들기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기