---
title: "Karpathy microgpt.py 해부: GPT 학습과 추론이 한 파일에서 돌아가는 방식"
slug: "karpathy-microgpt-line-by-line"
canonicalUrl: "https://moonshotnotes.com/posts/karpathy-microgpt-line-by-line/"
sourceUrl: "https://moonshotnotes.com/posts/karpathy-microgpt-line-by-line/"
markdownUrl: "https://moonshotnotes.com/agent/posts/karpathy-microgpt-line-by-line.md"
language: "ko"
category: "AI Development"
updatedAt: "2026-05-05"
agentTokenEstimate: 6789
---

# Karpathy microgpt.py 해부: GPT 학습과 추론이 한 파일에서 돌아가는 방식

Andrej Karpathy의 microgpt.py를 한 파일짜리 GPT 실행체로 해부합니다. 문자 tokenizer, scalar autograd, Q/K/V attention, Adam update, autoregressive sampling이 어떻게 연결되는지 설명합니다.

## Agent metadata

- Source: https://moonshotnotes.com/posts/karpathy-microgpt-line-by-line/
- Markdown: https://moonshotnotes.com/agent/posts/karpathy-microgpt-line-by-line.md
- Language: ko
- Category: AI Development
- Tags: Karpathy, GPT, Transformer, Autograd, Python
- Updated: 2026-05-05
- Estimated tokens: 6789

GPT를 제대로 이해하려고 코드를 열면, 의외로 본질보다 주변 장치가 먼저 쏟아집니다.

PyTorch, CUDA, tokenizer, dataset loader, optimizer, scheduler, checkpoint, distributed training. 실무에서는 모두 필요한 장치입니다. 하지만 “GPT는 실제로 무엇을 계산하는가”를 처음 붙잡으려는 순간에는 이 장치들이 시야를 가립니다.

Andrej Karpathy의 `microgpt.py`가 좋은 이유는 작아서가 아닙니다. **숨겨진 레이어를 거의 모두 벗겨내고, GPT 학습 루프를 한 파일 안에서 끝까지 보이게 만들기 때문**입니다. 문자 tokenizer에서 시작해 scalar autograd, Q/K/V attention, negative log likelihood, Adam update, autoregressive sampling까지 이어지는 경로가 그대로 드러납니다.

이 글의 목표는 “작은 GPT 구현을 소개하는 것”이 아닙니다. `loss.backward()`와 `optimizer.step()` 뒤에서 어떤 계산이 벌어지는지, attention이 왜 Q/K/V로 나뉘는지, 다음 token 학습이 실제 코드에서는 어떤 루프로 바뀌는지 확인하는 것입니다.

`microgpt.py`는 200라인 안팎의 짧은 파일입니다. 그래서 이 글은 원문을 피해서 설명하기보다, **원문 흐름을 따라 핵심 코드 블록을 충분히 싣고 바로 아래에서 해체하는 방식**으로 읽습니다. 발행 시점의 최신 원본은 [Karpathy의 microgpt.py gist](https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95)에서 다시 확인하는 편이 안전합니다.

분석 기준일: **2026-05-05**<br />
분석 대상: Andrej Karpathy `microgpt.py` gist<br />
분석 방식: 원문 흐름을 따른 핵심 코드 블록 발췌, 라인별 설명, GPT 구조와 연결<br />
주의사항: gist는 수정될 수 있으므로 실제 학습이나 재현 전에는 원본 revision을 다시 확인해야 합니다.

---

## 핵심 요약

- `microgpt.py`는 GPT를 “라이브러리 호출”이 아니라 **계산 그래프, attention, optimizer가 연결된 실행 루프**로 보여줍니다.
- PyTorch가 숨기는 `Tensor`, `autograd`, `Module`, `optimizer.step()`의 핵심 역할을 작은 Python 객체와 리스트로 직접 드러냅니다.
- 모델은 대형 LLM이 아니라 이름 데이터셋을 문자 단위로 학습하는 작은 decoder-only Transformer입니다. 그래서 구조는 보이지만, 성능은 목적이 아닙니다.
- attention은 `Q`, `K`, `V`, scaled dot product, softmax, weighted sum이라는 흐름으로 구현됩니다. 이 부분이 Transformer를 읽는 기준점입니다.
- 이 코드를 읽고 나면 GPT 학습은 “다음 token 확률을 높이도록 계산 그래프를 만들고, gradient로 weight를 고치는 과정”으로 정리됩니다.

> **주의:** 이 글의 코드 블록은 200라인 안팎의 원문 흐름을 따라 읽기 위한 발췌입니다. production용 GPT 구현이 아니라 학습용 reference를 해설하는 글입니다.

---

## 이 글을 읽는 순서

처음부터 끝까지 읽어도 되지만, 목적에 따라 보는 순서를 바꾸면 더 빠릅니다.

| 목표 | 먼저 볼 구간 |
|---|---|
| PyTorch autograd가 궁금하다 | `Value` 클래스, local gradient, `backward()` |
| Transformer attention이 궁금하다 | Q/K/V 생성, multi-head attention, KV cache |
| GPT 학습 루프가 궁금하다 | next-token 문제 변환, loss 계산, Adam update |
| 추론이 궁금하다 | BOS 시작, temperature softmax, sampling |

핵심 흐름만 보고 싶다면 `Value.backward()` → `gpt()` → loss 계산 → Adam update → sampling 순서로 보면 됩니다.

---

## 1. `microgpt.py`는 어떤 코드인가

이 코드를 한 문장으로 정의하면 다음과 같습니다.

```text
문자 단위 이름 데이터셋으로 작은 GPT를 학습하고,
새로운 이름처럼 보이는 문자열을 생성하는 순수 Python 실행체
```

원본 파일은 `os`, `math`, `random` 같은 표준 라이브러리만 사용합니다. 코드에는 데이터 다운로드, tokenizer 구성, autograd 엔진, Transformer forward pass, Adam update, inference sampling이 순서대로 들어 있습니다. 그래서 이 파일은 “작은 모델”이라기보다 **GPT 실행 경로를 관찰하기 위한 투명한 실험실**에 가깝습니다.

| 항목 | 내용 |
|---|---|
| 파일명 | `microgpt.py` |
| 작성자 | Andrej Karpathy |
| 구현 언어 | Python |
| 외부 ML 의존성 | 없음 |
| 데이터 | `names.txt` 형태의 이름 목록 |
| tokenizer | 문자 단위 tokenizer |
| 모델 구조 | 작은 decoder-only Transformer |
| 학습 목표 | 다음 문자 예측 |
| optimizer | 직접 구현한 Adam |
| 추론 방식 | autoregressive sampling |

핵심 메시지는 이렇습니다.

```text
GPT의 중심은 다음 token 확률을 높이는 계산이다.
다만 그 계산을 빠르고 크게 만들려면 별도의 시스템 엔지니어링이 필요하다.
```

---

## 2. 전체 실행 흐름

프로그램 흐름부터 잡고 가겠습니다.

```text
input.txt 확인
-> 없으면 names.txt 다운로드
-> docs 리스트 생성
-> 문자 vocabulary 생성
-> Value 클래스로 scalar autograd 구성
-> embedding / attention / MLP / lm_head weight 초기화
-> 문서 하나씩 next-token prediction 학습
-> loss.backward()
-> Adam으로 parameter update
-> BOS에서 시작해 문자 샘플 생성
```

이 흐름은 실제 GPT 학습의 축소판입니다.

실제 대형 모델에서는 데이터셋이 웹 규모이고, tokenizer가 BPE 계열이며, 연산은 GPU tensor kernel로 수행됩니다. 하지만 알고리즘의 중심은 여전히 같습니다.

```text
토큰을 벡터로 바꾼다
-> attention과 MLP를 통과시킨다
-> 다음 토큰 확률을 예측한다
-> 정답과 비교해 loss를 만든다
-> gradient를 계산한다
-> optimizer로 weight를 업데이트한다
-> 추론 때는 다음 토큰을 하나씩 샘플링한다
```

---

## 3. import와 seed 고정

가장 먼저 볼 부분은 import와 seed입니다.

```python
import os      # os.path.exists
import math    # math.log, math.exp
import random  # seed, choices, gauss, shuffle

random.seed(42)
```

이 네 줄은 단순해 보이지만 코드의 성격을 잘 보여줍니다.

| 코드 | 역할 |
|---|---|
| `os` | `input.txt` 파일 존재 여부를 확인합니다. |
| `math` | `log`, `exp` 같은 수학 함수를 사용합니다. |
| `random` | 데이터 셔플, weight 초기화, 샘플링에 사용합니다. |
| `random.seed(42)` | 실행 결과를 어느 정도 재현 가능하게 만듭니다. |

중요한 점은 `numpy`, `torch`, `tensorflow`가 없다는 것입니다. 즉 이 코드는 tensor library 없이도 GPT의 학습과 추론 흐름을 보여줍니다. 대신 모든 연산이 Python scalar와 list 위에서 일어나므로 속도는 매우 느립니다.

실무적으로는 이런 방식으로 학습하지 않습니다. 하지만 PyTorch가 내부에서 어떤 일을 대신해주는지 이해하기에는 좋은 형태입니다.

---

## 4. 데이터셋 준비

학습 데이터 준비는 대략 이런 모양입니다.

```python
if not os.path.exists('input.txt'):
    import urllib.request
    names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt'
    urllib.request.urlretrieve(names_url, 'input.txt')

docs = [line.strip() for line in open('input.txt') if line.strip()]
random.shuffle(docs)
print(f"num docs: {len(docs)}")
```

원본 코드는 `input.txt`가 없으면 `makemore`의 `names.txt`를 내려받고, 각 줄을 하나의 document로 읽어 `docs` 리스트를 만듭니다.

| 단계 | 설명 |
|---|---|
| 파일 확인 | 현재 디렉터리에 학습 파일이 있는지 확인합니다. |
| 원격 다운로드 | 없으면 이름 데이터셋을 다운로드합니다. |
| 줄 단위 읽기 | 이름 하나를 document 하나로 봅니다. |
| 빈 줄 제거 | 의미 없는 입력을 제외합니다. |
| 셔플 | 학습 순서를 고정된 seed 아래에서 섞습니다. |

`docs`는 대략 이런 형태입니다.

```python
["emma", "olivia", "ava", "isabella"]
```

일반적인 GPT 학습에서 document라고 하면 웹페이지, 책, 코드 파일, 대화 기록 같은 긴 텍스트를 떠올릴 수 있습니다. 하지만 여기서는 이름 하나가 문서 하나입니다.

```text
doc = "emma"
```

이 모델은 문장을 생성하는 모델이 아니라, 이름처럼 보이는 문자열을 생성하는 character-level GPT입니다.

---

## 5. 문자 단위 tokenizer

다음은 tokenizer입니다.

```python
uchars = sorted(set(''.join(docs)))
BOS = len(uchars)
vocab_size = len(uchars) + 1

print(f"vocab size: {vocab_size}")
```

데이터셋에 등장하는 모든 고유 문자를 정렬하고, 마지막 index를 `BOS` 특수 토큰으로 사용합니다.

| 코드 | 설명 |
|---|---|
| `unique_characters(docs)` | 모든 이름에 등장한 문자를 모읍니다. |
| `sorted(...)` | 문자 순서를 고정합니다. |
| `BOS = len(uchars)` | 문자 index 다음 번호를 특수 토큰으로 둡니다. |
| `vocab_size = len(uchars) + 1` | 문자 수에 특수 토큰 1개를 더합니다. |

예를 들어 데이터셋에 알파벳 소문자 26개만 있다면 다음과 비슷합니다.

```text
uchars = ['a', 'b', 'c', ..., 'z']
BOS = 26
vocab_size = 27
```

실제 학습에서는 이름 앞뒤에 `BOS`를 붙입니다.

```python
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
```

예를 들어 `"emma"`는 개념적으로 이렇게 변환됩니다.

```text
[BOS, e, m, m, a, BOS]
```

첫 번째 `BOS`는 "이제 이름을 시작한다"는 의미입니다. 마지막 `BOS`는 "이름이 끝났다"는 의미로도 사용됩니다.

실무 tokenizer에서는 보통 `BOS`, `EOS`, `PAD`, `UNK` 등을 분리합니다. 이 코드는 단순성을 위해 하나의 특수 토큰을 시작과 종료 양쪽에 사용합니다.

---

## 6. `Value` 클래스와 scalar autograd

이 파일에서 가장 중요한 부분 중 하나는 `Value` 클래스입니다.

```python
class Value:
    __slots__ = ('data', 'grad', '_children', '_local_grads')

    def __init__(self, data, children=(), local_grads=()):
        self.data = data                  # forward 값
        self.grad = 0                     # loss에 대한 gradient
        self._children = children         # 계산 그래프의 이전 노드
        self._local_grads = local_grads   # 각 child에 대한 local derivative
```

이 객체 하나는 단순한 숫자처럼 보이지만 실제로는 네 가지 정보를 갖습니다.

| 필드 | 의미 |
|---|---|
| `data` | forward pass에서 계산된 실제 숫자 값 |
| `grad` | loss가 이 값에 대해 얼마나 민감한지 나타내는 gradient |
| `_children` | 이 값을 만들 때 사용된 이전 노드들 |
| `_local_grads` | 현재 노드가 child들에 대해 갖는 local derivative |

PyTorch로 비유하면 `Value`는 아주 작은 `Tensor`입니다. 다만 PyTorch Tensor는 다차원 배열이고, `Value`는 scalar 하나입니다.

| 구분 | `Value` | PyTorch Tensor |
|---|---|---|
| 단위 | scalar 하나 | 다차원 배열 |
| 연산 방식 | Python scalar 연산 | vectorized tensor 연산 |
| autograd | 직접 구현 | 프레임워크 내장 |
| 속도 | 느림 | 빠름 |
| 목적 | 교육용 | 실전 학습/추론 |

Karpathy의 별도 프로젝트인 [micrograd](https://github.com/karpathy/micrograd)도 scalar 값 위에서 동적으로 DAG를 만들고 reverse-mode autodiff를 수행하는 작은 autograd engine입니다.

결국 `microgpt.py`의 `Value` 클래스는 다음 질문에 답하기 위한 장치입니다.

```text
loss.backward()가 마법이 아니라면,
그 안에서는 정확히 무슨 일이 일어나는가?
```

---

## 7. 덧셈, 곱셈, local gradient

`Value`는 연산 결과를 새 `Value`로 만들면서 계산 그래프를 같이 저장합니다.

```python
def __add__(self, other):
    other = other if isinstance(other, Value) else Value(other)
    return Value(self.data + other.data, (self, other), (1, 1))

def __mul__(self, other):
    other = other if isinstance(other, Value) else Value(other)
    return Value(self.data * other.data, (self, other), (other.data, self.data))
```

덧셈부터 보면 forward 값은 `x + y`이고, local gradient는 양쪽 모두 1입니다.

```text
z = x + y

dz/dx = 1
dz/dy = 1
```

곱셈은 다릅니다.

```text
z = x * y

dz/dx = y
dz/dy = x
```

그래서 곱셈의 local gradient는 상대방 값입니다.

이 구조 덕분에 나중에 `loss.backward()`를 호출하면 계산 그래프를 거꾸로 따라가면서 모든 parameter의 gradient를 구할 수 있습니다.

---

## 8. `backward()`와 chain rule

autograd의 핵심은 `backward()`입니다.

```python
def backward(self):
    topo = []
    visited = set()

    def build_topo(v):
        if v not in visited:
            visited.add(v)
            for child in v._children:
                build_topo(child)
            topo.append(v)

    build_topo(self)
    self.grad = 1

    for v in reversed(topo):
        for child, local_grad in zip(v._children, v._local_grads):
            child.grad += local_grad * v.grad
```

이 함수는 세 단계로 이해하면 됩니다.

```text
1. loss에서 시작해 계산 그래프 전체를 수집한다.
2. 그래프를 위상 정렬한다.
3. 뒤에서 앞으로 돌면서 chain rule을 적용한다.
```

가장 중요한 줄은 이것입니다.

```python
child.grad += local_grad * v.grad
```

수식으로 쓰면 다음과 같습니다.

```text
dLoss/dChild += dCurrent/dChild * dLoss/dCurrent
```

즉 chain rule입니다. PyTorch의 다음 한 줄도 원리는 같습니다.

```python
loss.backward()
```

차이는 실행 단위입니다. PyTorch는 tensor 단위로 빠르게 처리하고, `microgpt.py`는 scalar `Value` 객체 단위로 느리지만 투명하게 처리합니다.

---

## 9. 모델 파라미터 초기화

이제 GPT 모델이 학습할 parameter를 만듭니다.

```python
n_layer = 1
n_embd = 16
block_size = 16
n_head = 4
head_dim = n_embd // n_head
```

| 설정 | 값 | 의미 |
|---|---:|---|
| `n_layer` | 1 | Transformer block 개수 |
| `n_embd` | 16 | token embedding 차원 |
| `block_size` | 16 | 최대 context 길이 |
| `n_head` | 4 | attention head 수 |
| `head_dim` | 4 | head 하나가 담당하는 차원 |

이 값들은 실제 LLM과 비교하면 극단적으로 작습니다. 하지만 구조는 GPT의 기본 형태를 유지합니다.

```text
embedding
-> attention
-> MLP
-> output logits
```

weight matrix도 일반 숫자가 아니라 `Value` 객체로 채웁니다.

```python
matrix = lambda nout, nin, std=0.08: [
    [Value(random.gauss(0, std)) for _ in range(nin)]
    for _ in range(nout)
]
```

이렇게 해야 forward pass 이후 각 weight가 자기 자신의 gradient를 가질 수 있습니다.

---

## 10. `state_dict`: GPT 구성 요소 모으기

파라미터는 `state_dict`에 저장됩니다.

```python
state_dict = {
    'wte': matrix(vocab_size, n_embd),
    'wpe': matrix(block_size, n_embd),
    'lm_head': matrix(vocab_size, n_embd),
}
```

| key | 역할 |
|---|---|
| `wte` | token embedding |
| `wpe` | position embedding |
| `lm_head` | hidden vector를 vocab logits로 바꾸는 출력층 |

Transformer layer 내부 weight는 다음 성격을 갖습니다.

| weight | 의미 |
|---|---|
| `attn_wq` | query projection |
| `attn_wk` | key projection |
| `attn_wv` | value projection |
| `attn_wo` | attention output projection |
| `mlp_fc1` | MLP 확장 projection |
| `mlp_fc2` | MLP 축소 projection |

MLP는 `n_embd`를 4배로 키웠다가 다시 줄입니다.

```text
16 -> 64 -> 16
```

마지막으로 모든 parameter를 1차원 리스트로 펼칩니다. optimizer가 모든 parameter를 순회하면서 업데이트하기 쉽게 만들기 위해서입니다.

---

## 11. `linear`, `softmax`, `rmsnorm`

모델 forward에서 반복적으로 쓰이는 helper는 세 개입니다.

```python
def linear(x, w):
    return [dot(row, x) for row in w]
```

이 함수는 행렬-벡터 곱입니다. PyTorch로 쓰면 대략 다음과 같습니다.

```python
y = W @ x
```

하지만 여기서는 tensor 연산이 아닙니다. 모든 곱셈과 덧셈은 `Value` 연산입니다. 따라서 `linear()`는 단순 계산이면서 동시에 autograd graph를 만듭니다.

다음은 softmax입니다.

```python
def softmax(logits):
    max_val = max(val.data for val in logits)
    exps = [(val - max_val).exp() for val in logits]
    total = sum(exps)
    return [e / total for e in exps]
```

softmax는 logits를 확률 분포로 바꿉니다.

```text
softmax(x_i) = exp(x_i) / sum(exp(x_j))
```

여기서 최대값을 먼저 빼는 이유는 numerical stability 때문입니다. 예를 들어 logits가 `[1000, 1001, 1002]`라면 그대로 지수 함수를 적용할 때 overflow가 날 수 있습니다. 최대값을 빼면 `[-2, -1, 0]`이 되고, softmax 결과는 동일하지만 계산은 훨씬 안정적입니다.

다음은 RMSNorm입니다.

```python
def rmsnorm(x):
    ms = sum(xi * xi for xi in x) / len(x)
    scale = (ms + 1e-5) ** -0.5
    return [xi * scale for xi in x]
```

RMSNorm은 입력 벡터의 root mean square를 기준으로 값을 정규화합니다. [RMSNorm 논문](https://arxiv.org/abs/1910.07467)은 LayerNorm에서 평균을 빼는 re-centering을 제거하고 RMS 통계 기반 정규화를 제안합니다.

---

## 12. `gpt()` 함수와 embedding

이제 모델 본체인 `gpt()` 함수입니다.

```python
def gpt(token_id, pos_id, keys, values):
    tok_emb = state_dict['wte'][token_id]
    pos_emb = state_dict['wpe'][pos_id]
    x = [t + p for t, p in zip(tok_emb, pos_emb)]
    x = rmsnorm(x)
```

| 코드 | 의미 |
|---|---|
| `wte[token_id]` | 현재 token의 embedding vector를 가져옵니다. |
| `wpe[pos_id]` | 현재 위치의 position embedding을 가져옵니다. |
| `tok_emb + pos_emb` | token 정보와 위치 정보를 더합니다. |
| `rmsnorm(x)` | 입력 벡터 크기를 정규화합니다. |

Transformer는 token id 자체를 바로 처리하지 않습니다. 먼저 token id를 vector로 바꿉니다.

```text
token_id -> token embedding vector
```

그런데 attention만으로는 token의 순서를 알 수 없습니다. 그래서 위치 정보도 같이 넣어야 합니다.

```text
token embedding + position embedding
```

[Transformer 논문](https://arxiv.org/abs/1706.03762)도 recurrence와 convolution이 없는 구조에서는 sequence 순서 정보를 사용하기 위해 positional encoding을 input embedding에 더한다고 설명합니다.

---

## 13. Q, K, V 만들기

Transformer block 안으로 들어가면 먼저 attention을 준비합니다.

```python
x_residual = x
x = rmsnorm(x)

q = linear(x, state_dict[f'layer{li}.attn_wq'])
k = linear(x, state_dict[f'layer{li}.attn_wk'])
v = linear(x, state_dict[f'layer{li}.attn_wv'])

keys[li].append(k)
values[li].append(v)
```

Q, K, V는 attention의 핵심 개념입니다.

| 이름 | 의미 | 직관 |
|---|---|---|
| Query | 현재 token이 찾고 싶은 정보 | "나는 무엇을 보고 싶은가?" |
| Key | 각 token이 가진 주소 또는 특징 | "나는 어떤 정보인가?" |
| Value | 실제로 전달될 내용 | "내가 줄 내용은 무엇인가?" |

`keys[layer].append(k)`와 `values[layer].append(v)`는 작은 KV cache입니다. 현재 위치까지의 key/value만 쌓이기 때문에 모델은 미래 token을 볼 수 없습니다.

일반적인 decoder-only Transformer에서는 causal mask를 사용해 미래 token을 가립니다. `microgpt.py`는 sequence 전체를 한 번에 넣는 방식이 아니라 왼쪽에서 오른쪽으로 token을 하나씩 처리합니다. 그래서 명시적인 causal mask 없이도 autoregressive 조건이 유지됩니다.

```text
현재 position에서 볼 수 있는 것:
[BOS, 이전 문자들, 현재 문자]

현재 position에서 볼 수 없는 것:
미래 문자들
```

---

## 14. Multi-head attention 해체

`n_embd = 16`, `n_head = 4`라면 head 하나의 차원은 4입니다.

```text
전체 embedding 16차원

head 0: 0~3
head 1: 4~7
head 2: 8~11
head 3: 12~15
```

하나의 큰 attention을 수행하는 대신 vector를 여러 조각으로 나누어 여러 관점에서 attention을 수행합니다.

```python
for h in range(n_head):
    hs = h * head_dim
    q_h = q[hs:hs+head_dim]
    k_h = [ki[hs:hs+head_dim] for ki in keys[li]]
    v_h = [vi[hs:hs+head_dim] for vi in values[li]]
```

attention score는 scaled dot-product로 계산합니다.

```python
attn_logits = [
    sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5
    for t in range(len(k_h))
]
attn_weights = softmax(attn_logits)
```

[Transformer 논문](https://arxiv.org/abs/1706.03762)은 query와 key의 dot product를 key 차원의 제곱근으로 나눈 뒤 softmax를 적용해 value에 대한 weight를 만든다고 설명합니다.

그 다음 value를 가중합합니다.

```python
head_out = [
    sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h)))
    for j in range(head_dim)
]
x_attn.extend(head_out)
```

즉 attention은 다음 질문에 답합니다.

```text
현재 token이 다음 token을 예측하려면,
이전 token들 중 무엇을 얼마나 참고해야 하는가?
```

---

## 15. Attention output과 residual connection

각 head의 결과를 이어붙인 뒤에는 output projection을 수행합니다.

```python
x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])
x = x + x_residual
```

첫 줄은 여러 head의 출력을 다시 embedding 차원으로 섞습니다.

```text
multi-head output -> output projection
```

두 번째 줄은 residual connection입니다.

```text
x = attention_output + original_input
```

Residual connection은 깊은 네트워크에서 정보와 gradient가 잘 흐르게 도와줍니다. Transformer 논문도 각 sub-layer 주변에 residual connection을 사용합니다.

`microgpt.py`에서는 layer가 1개뿐이지만 구조 자체는 GPT block의 기본 패턴을 따릅니다.

```text
RMSNorm
-> Multi-head Attention
-> Output Projection
-> Residual Add
```

---

## 16. MLP block과 `lm_head`

Attention 다음에는 MLP block이 옵니다.

```python
x_residual = x
x = rmsnorm(x)

x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
x = [xi.relu() for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2'])

x = x + x_residual
```

구조를 한 줄로 쓰면 이렇습니다.

```text
x -> RMSNorm -> Linear -> ReLU -> Linear -> Residual Add
```

크기로 보면 다음과 같습니다.

```text
16 -> 64 -> 16
```

Attention이 "어떤 과거 token을 볼지"를 정한다면, MLP는 각 위치의 hidden representation을 비선형적으로 변환합니다. Transformer 논문도 attention sub-layer 외에 position-wise feed-forward network를 둡니다.

마지막으로 output head를 통과합니다.

```python
logits = linear(x, state_dict['lm_head'])
return logits
```

`logits`는 아직 확률이 아닙니다.

```text
logits = 다음 token 후보들에 대한 원시 점수
```

예를 들어 `vocab_size = 27`이라면 logits도 27개입니다. 여기에 softmax를 적용하면 다음 token 확률이 됩니다.

```python
probs = softmax(logits)
```

결국 `gpt()` 함수는 다음 역할을 합니다.

```text
현재 token, 현재 position, 과거 KV cache를 받아
다음 token에 대한 logits를 반환한다.
```

---

## 17. 학습 루프: 이름 하나를 next-token 문제로 바꾸기

학습 루프는 각 step에서 문서 하나를 고르고, 앞뒤에 `BOS`를 붙인 token sequence를 만듭니다.

```python
for step in range(num_steps):
    doc = docs[step % len(docs)]
    tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
    n = min(block_size, len(tokens) - 1)
```

| 코드 | 의미 |
|---|---|
| `docs[step % len(docs)]` | 문서를 하나 선택합니다. |
| `encode(doc)` | 문자를 token id로 변환합니다. |
| `[BOS] + ... + [BOS]` | 시작과 종료 토큰을 붙입니다. |
| `min(block_size, ...)` | 최대 context 길이를 넘지 않게 자릅니다. |

예를 들어 `doc = "emma"`라면 다음 sequence가 됩니다.

```text
[BOS, e, m, m, a, BOS]
```

이 sequence는 학습 중 다음 문제들로 바뀝니다.

| 입력 token | 정답 token |
|---|---|
| `BOS` | `e` |
| `e` | `m` |
| `m` | `m` |
| `m` | `a` |
| `a` | `BOS` |

GPT 학습의 기본 목표는 이것입니다.

```text
지금까지 본 token들을 바탕으로 다음 token을 맞혀라.
```

---

## 18. loss 계산

forward pass와 loss 계산은 다음 구조입니다.

```python
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
losses = []

for pos_id in range(n):
    token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
    logits = gpt(token_id, pos_id, keys, values)
    probs = softmax(logits)
    loss_t = -probs[target_id].log()
    losses.append(loss_t)

loss = (1 / n) * sum(losses)
```

핵심은 정답 token 확률의 negative log likelihood입니다.

```text
loss = -log(P(correct_next_token))
```

정답 token 확률이 높으면 loss는 작아집니다.

```text
P(correct) = 0.9
-log(0.9) ~= 0.105
```

정답 token 확률이 낮으면 loss는 커집니다.

```text
P(correct) = 0.01
-log(0.01) ~= 4.605
```

즉 모델은 실제 다음 문자에 더 높은 확률을 주도록 weight를 바꾸는 방향으로 학습됩니다.

---

## 19. `loss.backward()`: 모든 parameter로 gradient 흘려보내기

loss가 만들어지면 역전파를 실행합니다.

```python
loss.backward()
```

이 한 줄은 앞에서 만든 `Value.backward()`를 호출합니다. 실제로는 다음 경로를 거꾸로 따라갑니다.

```text
loss
-> log
-> softmax
-> logits
-> lm_head
-> MLP
-> attention
-> embedding
-> parameters
```

각 parameter는 `Value` 객체입니다. 따라서 `loss.backward()`가 끝나면 각 parameter의 `grad` 필드가 채워집니다.

```python
p.grad
```

이 값은 다음 의미를 가집니다.

```text
이 parameter를 조금 바꾸면 loss가 얼마나 변하는가?
```

실무적으로 PyTorch의 `loss.backward()`를 이해할 때도 이 관점이 중요합니다. 이 한 줄은 마법이 아니라, 계산 그래프를 거꾸로 따라가며 chain rule을 적용하는 과정입니다.

---

## 20. Adam optimizer 직접 구현

이제 gradient를 이용해 parameter를 업데이트합니다.

```python
learning_rate = 0.01
beta1 = 0.85
beta2 = 0.99
eps_adam = 1e-8

m = [0.0] * len(params)
v = [0.0] * len(params)
```

[Adam 논문](https://arxiv.org/abs/1412.6980)은 Adam을 gradient의 lower-order moments에 대한 adaptive estimates를 사용하는 stochastic optimization 알고리즘으로 소개합니다.

| 변수 | 의미 |
|---|---|
| `learning_rate` | parameter를 한 번에 얼마나 움직일지 결정합니다. |
| `beta1` | gradient 이동 평균의 decay 계수입니다. |
| `beta2` | gradient 제곱 이동 평균의 decay 계수입니다. |
| `eps` | 0으로 나누는 것을 방지합니다. |
| `m` | first moment buffer입니다. |
| `v` | second moment buffer입니다. |

업데이트는 다음 절차로 진행됩니다.

```text
1. step이 진행될수록 learning rate를 줄인다.
2. gradient의 이동 평균 m을 업데이트한다.
3. gradient 제곱의 이동 평균 v를 업데이트한다.
4. 초기 step에서 생기는 bias를 보정한다.
5. parameter 값을 직접 수정한다.
6. gradient를 0으로 초기화한다.
```

원문 update loop의 핵심은 이 부분입니다.

```python
lr_t = learning_rate * (1 - step / num_steps)

for i, p in enumerate(params):
    m[i] = beta1 * m[i] + (1 - beta1) * p.grad
    v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2

    m_hat = m[i] / (1 - beta1 ** (step + 1))
    v_hat = v[i] / (1 - beta2 ** (step + 1))

    p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
    p.grad = 0
```

마지막 줄은 특히 중요합니다.

```python
p.grad = 0
```

gradient를 초기화하지 않으면 다음 step의 gradient가 이전 step에 누적됩니다. PyTorch로 치면 다음 코드와 비슷한 역할입니다.

```python
optimizer.zero_grad()
optimizer.step()
```

---

## 21. 추론: BOS에서 시작해 한 글자씩 생성하기

학습이 끝나면 새로운 이름을 생성합니다.

```python
temperature = 0.5

for sample_idx in range(20):
    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
    token_id = BOS
    sample = []
```

생성 루프는 다음과 같습니다.

```python
for pos_id in range(block_size):
    logits = gpt(token_id, pos_id, keys, values)
    probs = softmax([l / temperature for l in logits])

    token_id = random.choices(
        range(vocab_size),
        weights=[p.data for p in probs]
    )[0]

    if token_id == BOS:
        break

    sample.append(uchars[token_id])
```

생성 흐름은 다음과 같습니다.

```text
BOS
-> 첫 글자 샘플링
-> 두 번째 글자 샘플링
-> ...
-> BOS가 나오면 종료
```

temperature는 생성의 무작위성을 조절합니다.

| temperature | 효과 |
|---:|---|
| 낮음 | 높은 확률 token을 더 강하게 선호합니다. 결과가 안정적이지만 단조로울 수 있습니다. |
| 높음 | 낮은 확률 token도 선택될 가능성이 커집니다. 결과가 다양하지만 이상해질 수 있습니다. |

여기서는 `temperature = 0.5`입니다. 완전히 무작위로 생성하기보다는, 모델이 그럴듯하다고 판단한 문자를 더 강하게 선호합니다.

---

## 22. 전체 구조를 한 번에 다시 보기

`microgpt.py`의 전체 구조를 다시 요약하면 이렇습니다.

| 구간 | 코드 요소 | GPT 관점의 의미 |
|---|---|---|
| 데이터 준비 | `docs` | 학습 문서 목록 |
| tokenizer | `uchars`, `BOS`, `vocab_size` | 문자열을 token id로 변환 |
| autograd | `Value` | scalar 기반 계산 그래프와 gradient |
| parameter | `state_dict` | 모델이 학습할 weight |
| embedding | `wte`, `wpe` | token과 position을 vector로 변환 |
| attention | Q/K/V projection | 과거 token을 참고 |
| MLP | `mlp_fc1`, `mlp_fc2` | hidden representation 변환 |
| output | `lm_head` | 다음 token logits 생성 |
| loss | `-log(prob[target])` | 정답 token 확률을 높이는 목적 함수 |
| backward | `loss.backward()` | gradient 계산 |
| optimizer | Adam update | parameter 수정 |
| inference | 확률 샘플링 | 다음 token 생성 |

핵심을 한 문장으로 줄이면 이렇습니다.

```text
microgpt.py는 GPT의 학습과 추론을 PyTorch 없이 손으로 추적할 수 있게 만든 실행 가능한 해부도다.
```

실제 LLM과의 차이도 분명히 해야 합니다.

| 항목 | `microgpt.py` | 실제 LLM |
|---|---|---|
| tokenizer | 문자 단위 | BPE, SentencePiece, tiktoken 등 |
| 연산 단위 | scalar `Value` | tensor |
| 학습 데이터 | 이름 목록 | 대규모 corpus |
| 모델 크기 | 수천 parameter 수준 | 수십억에서 수조 parameter 가능 |
| 학습 방식 | 문서 하나씩 순차 처리 | batch, distributed training |
| 가속 | 없음 | GPU/TPU, fused kernel |
| 목적 | 교육용 | production 또는 research |

---

## 23. 이 코드에서 특히 배울 만한 것

이 코드를 단순히 "작은 GPT 구현"으로만 보면 아쉽습니다. 더 중요한 학습 포인트는 세 가지입니다. 이 세 가지가 잡히면, PyTorch로 작성된 Transformer 코드도 훨씬 덜 불투명해집니다.

### 23.1 Autograd는 계산 그래프와 chain rule이다

`Value` 클래스는 PyTorch autograd의 축소판입니다.

```text
forward 때 graph 생성
-> backward 때 chain rule 적용
-> parameter.grad 채우기
```

이 과정을 직접 보면 다음 코드가 훨씬 명확해집니다.

```python
loss.backward()
optimizer.step()
optimizer.zero_grad()
```

### 23.2 Attention은 Q와 K의 비교, V의 가중합이다

attention의 핵심은 세 줄입니다.

```text
score = q dot k / sqrt(d)
weight = softmax(score)
output = sum(weight * value)
```

즉 현재 token은 과거 token들을 모두 똑같이 보는 것이 아니라, 필요한 token에 더 높은 weight를 줍니다.

### 23.3 GPT 학습은 다음 token 확률을 높이는 과정이다

학습 목표는 복잡하지 않습니다.

```text
현재까지 본 token들로 다음 token을 맞힌다.
```

loss는 정답 token 확률의 negative log likelihood입니다.

```text
loss = -log(P(correct_next_token))
```

이 원리는 작은 이름 생성 모델에서도, 대형 언어 모델에서도 중심 아이디어로 남아 있습니다.

---

## 24. 실전 체크리스트

원본 코드를 직접 따라갈 때는 아래 순서로 보면 좋습니다.

```text
microgpt.py 읽기 체크리스트

[ ] 원본 gist 기준일을 확인했다.
[ ] input.txt가 없을 때 names.txt를 내려받는 흐름을 이해했다.
[ ] docs 하나가 이름 하나라는 점을 이해했다.
[ ] uchars, BOS, vocab_size가 tokenizer 역할을 한다는 점을 이해했다.
[ ] Value.data와 Value.grad의 차이를 설명할 수 있다.
[ ] add, multiply가 local gradient를 저장하는 방식을 이해했다.
[ ] backward()의 topological sort와 chain rule을 설명할 수 있다.
[ ] wte, wpe, lm_head의 역할을 구분할 수 있다.
[ ] Q, K, V projection의 역할을 설명할 수 있다.
[ ] attention score = q dot k / sqrt(head_dim)을 이해했다.
[ ] keys, values list가 작은 KV cache 역할을 한다는 점을 이해했다.
[ ] MLP block이 16 -> 64 -> 16 구조라는 점을 이해했다.
[ ] loss = -log(prob[target])의 의미를 설명할 수 있다.
[ ] Adam의 m, v, bias correction을 대략 설명할 수 있다.
[ ] inference에서 temperature가 하는 일을 이해했다.
```

---

## 25. Q&A

### Q1. 이 코드는 진짜 GPT인가?

구조적으로는 decoder-only Transformer 기반 next-token model입니다. 다만 크기가 매우 작고, tokenizer도 문자 단위이며, 학습 데이터도 이름 목록입니다.

그래서 실전 GPT 구현이라기보다는 GPT 구조를 이해하기 위한 교육용 미니어처라고 보는 것이 정확합니다.

### Q2. 왜 PyTorch를 쓰지 않았나?

목적이 성능이 아니라 알고리즘 설명이기 때문입니다.

PyTorch를 사용하면 `Tensor`, `Module`, `autograd`, `optimizer`가 많은 일을 감춰줍니다. 실무에서는 그게 장점입니다. 하지만 학습 목적에서는 오히려 내부 구조가 보이지 않을 수 있습니다.

`microgpt.py`는 그 감춰진 부분을 일부러 드러냅니다.

### Q3. 왜 `BOS`를 끝 token으로도 쓰나?

단순화 때문입니다.

실제 tokenizer에서는 보통 `BOS`와 `EOS`를 분리합니다. 하지만 이 코드는 이름 생성이라는 작은 문제를 다루므로, 하나의 특수 token을 시작과 종료에 모두 사용합니다.

### Q4. causal mask가 없는데 미래 token을 보지 않나?

이 코드는 sequence 전체를 한 번에 처리하지 않습니다. 각 token을 왼쪽에서 오른쪽으로 하나씩 처리하고, 현재까지의 key/value만 cache에 쌓습니다.

따라서 현재 position에서는 미래 token의 key/value가 아직 존재하지 않습니다.

```text
미래를 mask로 가리는 대신,
아예 미래를 계산하지 않은 상태에서 진행한다.
```

### Q5. 이 코드를 그대로 키우면 LLM이 되나?

개념적으로는 방향이 맞지만, 그대로는 어렵습니다. 실제 LLM에는 다음 요소들이 필요합니다.

```text
대규모 tokenizer
대규모 dataset pipeline
batch training
tensor 연산
GPU/TPU 가속
mixed precision
distributed training
checkpoint
evaluation
serving stack
보안 및 비용 관리
```

`microgpt.py`는 알고리즘의 뼈대를 보여주는 코드입니다. 실전 LLM은 그 위에 거대한 시스템 엔지니어링이 붙습니다.

### Q6. 이 코드에서 가장 중요한 줄은 무엇인가?

개인적으로는 세 가지 흐름입니다.

```python
child.grad += local_grad * node.grad
```

이 줄은 autograd의 핵심인 chain rule입니다.

```text
attention_score = q dot k / sqrt(head_dim)
```

이 식은 attention의 핵심입니다.

```text
loss = -log(P(correct_next_token))
```

이 식은 next-token prediction 학습 목표를 보여줍니다.

---

## 26. 참고자료와 불확실성

### 참고자료

- [Andrej Karpathy `microgpt.py` gist](https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95): 순수 Python으로 GPT 학습과 추론 과정을 한 파일에 담고 있습니다.
- [Karpathy `micrograd`](https://github.com/karpathy/micrograd): scalar-valued autograd engine으로, 동적으로 생성된 DAG 위에서 reverse-mode autodiff를 구현합니다.
- [Attention Is All You Need](https://arxiv.org/abs/1706.03762): scaled dot-product attention, multi-head attention, residual connection, feed-forward network, positional encoding의 기본 구조를 설명합니다.
- [Adam: A Method for Stochastic Optimization](https://arxiv.org/abs/1412.6980): Adam optimizer의 first moment, second moment, bias correction 아이디어를 설명합니다.
- [Root Mean Square Layer Normalization](https://arxiv.org/abs/1910.07467): RMS 통계 기반 정규화와 re-scaling invariance를 설명합니다.

### 확인된 사실

- 2026-05-05 확인 기준 원본 gist에는 `microgpt.py` 파일이 포함되어 있고, GitHub Gist 페이지에는 `Last active May 5, 2026 01:01`로 표시됩니다.
- 원본 코드에는 데이터 로딩, tokenizer, `Value` autograd, Transformer forward, Adam 학습 루프, inference sampling이 포함되어 있습니다.
- 원본 코드 주석은 이 모델이 GPT-2를 따르되 LayerNorm 대신 RMSNorm, bias 없음, GeLU 대신 ReLU를 사용한다고 설명합니다.

### 작성자의 해석

- 이 코드는 GPT를 실제로 서비스하기 위한 구현이 아니라, GPT 알고리즘을 손으로 따라가기 위한 교육용 reference에 가깝습니다.
- `Value` 클래스는 PyTorch autograd를 이해하는 데 좋은 출발점입니다.
- Attention 구현은 tensor operation이 아니라 Python list와 scalar 연산으로 되어 있어 느리지만 구조를 읽기 좋습니다.

### 불확실성

- gist는 수정될 수 있으므로 실제 학습이나 재현 전에는 원본 revision을 다시 확인해야 합니다.
- 실행 시간은 Python 버전, CPU 성능, 로컬 환경에 따라 달라질 수 있습니다.
- 이 글의 해설은 2026-05-05에 확인한 원본 gist 구조를 기준으로 합니다.

---

## 마무리

정리하면, `microgpt.py`는 GPT를 작게 만든 코드라기보다 **GPT를 이해하기 위해 불필요한 장치를 걷어낸 코드**에 가깝습니다.

실제 LLM을 만들려면 훨씬 더 많은 시스템이 필요합니다. tokenizer도 달라지고, 학습 데이터도 커지고, batch training, GPU kernel, distributed training, checkpoint, serving stack이 붙습니다. 하지만 GPT의 중심 흐름은 이 파일 안에 거의 다 들어 있습니다.

```text
문자 -> token
token -> embedding
embedding -> attention
attention -> MLP
MLP -> logits
logits -> loss
loss -> gradient
gradient -> Adam update
BOS -> sampling -> generated text
```

처음 읽을 때는 `gpt()` 함수보다 `Value.backward()`를 먼저 보는 것이 좋습니다. 이유는 단순합니다. 이 파일의 가장 큰 가치는 “Transformer를 구현했다”가 아니라, **forward 계산이 어떻게 graph가 되고, 그 graph가 어떻게 gradient로 돌아오는지**를 보여준다는 데 있습니다.

그 다음 `linear()`, `softmax()`, attention score 계산, loss 계산 순서로 따라가면 전체 구조가 선명해집니다. 이 코드를 이해했다면 PyTorch로 작성된 Transformer 코드도 더 이상 `loss.backward()`와 `optimizer.step()`이라는 두 줄의 마법처럼 보이지 않을 것입니다.
