GPT Study
GPT Study
강의 목표
Andrej Karpathy의 강의는 Tiny Shakespeare 데이터와 character-level tokenizer에서 출발해 next-token prediction을 수행하는 작은 GPT를 PyTorch로 직접 구현하고, Bigram language model부터 causal self-attention과 Transformer block까지 차례로 추가하면서 각 구성 요소가 필요한 이유를 확인한다.
GPT와 확률적 생성
GPT는 Generative Pre-trained Transformer의 약자로, Transformer를 기반으로 다음 token의 확률분포를 예측하며 이 강의에서 구현하는 작은 언어 모델도 같은 원리를 따른다.
1. 텍스트를 학습 데이터로 바꾸기
강의에서는 데이터에 등장하는 고유 문자를 vocabulary로 만들고 정수 ID와 양방향으로 매핑한 뒤, 전체 텍스트를 token ID tensor로 변환해 train/validation split을 구성한다.
Character-level tokenization
| 요소 | 역할 |
|---|---|
stoi | 문자 → 정수 token ID 변환 |
itos | token ID → 문자 복원 |
vocab_size | 데이터에 등장한 고유 문자 수 |
Context와 batch
block_size는 모델이 한 번에 볼 수 있는 최대 context 길이다. 입력 \(x\)와 정답 \(y\)는 한 token만큼 어긋나 있으며, 각 위치에서 지금까지의 context로 바로 다음 token을 예측하도록 학습한다. batch_size는 이런 독립적인 sequence를 병렬로 몇 개 처리할지 정한다.
| Tensor | Shape | 의미 |
|---|---|---|
| \(x, y\) | \((B, T)\) | Token ID로 이루어진 입력과 다음-token 정답 |
| Embedding | \((B, T, C)\) | 각 token을 \(C\)차원 벡터로 표현한 결과 |
데이터 준비 핵심 코드 보기
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}
encode = lambda s: [stoi[c] for c in s]
decode = lambda ids: ''.join(itos[i] for i in ids)
def get_batch(split):
source = train_data if split == 'train' else val_data
ix = torch.randint(len(source) - block_size, (batch_size,))
x = torch.stack([source[i:i + block_size] for i in ix])
y = torch.stack([source[i + 1:i + block_size + 1] for i in ix])
return x, y2. Bigram model로 기준선 만들기
가장 단순한 모델은 현재 token 하나만 보고 다음 token을 예측한다. nn.Embedding(vocab_size, vocab_size)의 각 행은 현재 token에 대응하며, 각 열의 값은 다음 token 후보에 대한 logits다. Softmax를 적용하기 전이므로 이 값 자체가 확률은 아니다.
학습할 때는 logits의 \((B, T, C)\)를 \((B \times T, C)\)로, target의 \((B, T)\)를 \((B \times T)\)로 펼친 뒤 cross-entropy loss를 계산한다. 생성 단계에서는 마지막 timestep의 logits를 확률로 바꾸고 다음 token을 sampling해 기존 sequence 뒤에 붙인다.
Bigram 기준선 코드 보기
class BigramLanguageModel(nn.Module):
def __init__(self, vocab_size):
super().__init__()
self.token_embedding_table = nn.Embedding(vocab_size, vocab_size)
def forward(self, idx, targets=None):
logits = self.token_embedding_table(idx) # (B, T, C)
if targets is None:
return logits, None
B, T, C = logits.shape
loss = F.cross_entropy(logits.view(B * T, C), targets.view(B * T))
return logits, loss3. Self-Attention으로 context를 읽기
Self-Attention은 각 token이 같은 sequence 안의 다른 token을 얼마나 참고할지 계산한다. Query와 Key의 내적으로 관계 점수를 만들고, causal mask로 미래 token을 가린 뒤 softmax weight로 Value를 가중 합산한다.
| 요소 | 질문 | 기능 |
|---|---|---|
| Query | 현재 token이 무엇을 찾는가? | 참고할 context의 조건 표현 |
| Key | 각 token이 무엇을 제공할 수 있는가? | Query와 비교해 attention score 생성 |
| Value | 선택된 token에서 어떤 정보를 가져오는가? | Attention weight에 따라 합산되는 실제 내용 |
왜 \(\sqrt{d_k}\)로 나누는가?
Query와 Key의 차원이 커질수록 내적의 분산도 커진다. 큰 score를 그대로 softmax에 넣으면 분포가 한 위치에 지나치게 몰려 gradient가 불안정해질 수 있으므로, \(\sqrt{d_k}\)로 나눠 scale을 맞춘다.
Causal Self-Attention 핵심 코드 보기
k = self.key(x)
q = self.query(x)
v = self.value(x)
wei = q @ k.transpose(-2, -1) * (k.size(-1) ** -0.5)
wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf'))
wei = F.softmax(wei, dim=-1)
wei = self.dropout(wei)
out = wei @ v4. Transformer Block 조립하기
Karpathy는 Transformer block을 token 사이의 communication을 담당하는 Multi-Head Self-Attention과, 각 token이 얻은 정보를 개별적으로 처리하는 computation 단계인 Feed-Forward Network의 조합으로 설명한다.
| 구성 요소 | 핵심 역할 |
|---|---|
| Multi-Head Attention | 여러 attention subspace에서 서로 다른 관계를 병렬로 학습한 뒤 결합 |
| Feed-Forward Network | 각 token 벡터를 독립적으로 변환하고 내부 차원을 확장한 뒤 축소 |
| Residual Connection | \(x + F(x)\) 경로로 원본 정보와 gradient 흐름 보존 |
| Pre-Norm | Attention과 Feed-Forward 전에 LayerNorm을 적용해 학습 안정성 향상 |
| Dropout | Attention weight와 projection 경로 일부를 무작위로 꺼 과적합 완화 |
Pre-Norm Transformer Block 코드 보기
class Block(nn.Module):
def __init__(self, n_embd, n_head):
super().__init__()
head_size = n_embd // n_head
self.sa = MultiHeadAttention(n_head, head_size)
self.ffwd = FeedForward(n_embd)
self.ln1 = nn.LayerNorm(n_embd)
self.ln2 = nn.LayerNorm(n_embd)
def forward(self, x):
x = x + self.sa(self.ln1(x))
x = x + self.ffwd(self.ln2(x))
return x5. 구현 결과는 Decoder-only Transformer
원 논문의 Transformer는 encoder와 decoder로 구성된다. Encoder는 입력 전체를 양방향으로 읽고, decoder는 causal mask를 사용하며 encoder output을 cross-attention으로 참고한다. 반면 이 강의에서 만든 언어 모델은 encoder와 cross-attention 없이 masked self-attention만 쌓은 decoder-only 구조다.
6. GPT 학습 단계와 연결하기
여기서 구현한 next-token predictor는 대규모 corpus로 base language model을 만드는 pretraining의 축소판으로 볼 수 있다.
Pretraining
대량의 text corpus에서 다음 token을 예측하며 언어의 통계적 구조를 학습하는 단계로, Karpathy의 nanoGPT는 이 과정을 재현하는 데 초점을 둔다.
Post-training
Instruction data와 선호도 데이터로 사용자 의도에 맞게 모델을 조정하는 단계로, 원문 노트의 labeling → reward model → PPO 흐름은 RLHF 기반 post-training에 해당한다.
댓글 남기기