오늘도 공부
Microgpt 코드 분석 #2 본문
Microgpt 분석 #1
microgptThis is a brief guide to my new art project microgpt, a single file of 200 lines of pure Python with no dependencies that trains and inferences a GPT. This file contains the full algorithmic content of what is needed: dataset of documents, tokenize
javaexpert.tistory.com
microgpt
This is a brief guide to my new art project microgpt, a single file of 200 lines of pure Python with no dependencies that trains and inferences a GPT. This file contains the full algorithmic content of what is needed: dataset of documents, tokenizer, autog
karpathy.github.io
Step 0) 파일 머리말 + import + 랜덤 시드
"""
The most atomic way to train and run inference for a GPT in pure, dependency-free Python.
This file is the complete algorithm.
Everything else is just efficiency.
@karpathy
"""
import os # os.path.exists
import math # math.log, math.exp
import random # random.seed, random.choices, random.gauss, random.shuffle
random.seed(42) # Let there be order among chaos
자세한 설명
- 맨 위의 """ ... """는 **설명 문장(주석)**이야. 실행에는 영향이 없고 “이 파일은 GPT 학습/생성의 가장 최소 단위 구현이다”라고 선언하는 부분.
- import os: 파일이 존재하는지 확인할 때 os.path.exists를 쓰기 위해 가져와.
- import math: log, exp 같은 수학 함수를 쓰기 위해 가져와.
- import random: 랜덤 초기화/샘플링/셔플/정규분포 난수 등을 쓰기 위해 가져와.
- random.seed(42): **랜덤을 ‘고정’**하는 주문이야.
- 같은 seed면 매번 실행할 때 처음 가중치(모델의 뇌)도 같고, 결과도 비슷하게 나와서 “공부/디버깅”에 좋다.
- seed가 없으면 실행할 때마다 랜덤이 달라져서 결과가 계속 바뀌어 헷갈릴 수 있어.
✅ 핵심 포인트
- 외부 라이브러리 없음(numpy/torch 없이 순수 파이썬).
- random.seed(42) 덕분에 재현 가능한 실험이 된다.
Step 1) Dataset 만들기: input.txt 다운로드 + docs 로드 + 셔플
# Let there be a Dataset `docs`: list[str] of documents (e.g. a list of names)
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)}")
자세한 설명
- 이 코드의 목표는 docs라는 리스트를 만드는 거야.
- docs: list[str] 형태 → 문서(문장/이름)들의 목록
- 여기서는 “이름 한 줄”이 “문서 하나”가 돼.
1) 파일이 없으면 다운로드
- if not os.path.exists('input.txt'):
input.txt가 없으면, - urllib.request.urlretrieve(names_url, 'input.txt')
인터넷에서 이름 목록 파일을 내려받아 input.txt로 저장해.
2) 파일 읽어서 docs 만들기
- open('input.txt')로 파일을 줄 단위로 읽고,
- line.strip()는 줄 끝의 \n 같은 공백을 제거해.
- if line.strip()는 “빈 줄은 제외”하겠다는 뜻.
3) 섞기(셔플)
- random.shuffle(docs)는 학습 순서를 섞어.
- 공부할 때 문제집을 항상 같은 순서로만 풀면 편향될 수 있잖아? 그걸 막는 느낌.
4) 개수 출력
- num docs: ...는 문서(이름) 개수를 보여줘.
✅ 핵심 포인트
- docs는 학습 데이터 전체(이름 리스트).
- shuffle은 학습을 골고루 하게 해준다.
Step 2) Tokenizer 만들기: 문자 집합 + BOS + vocab_size
# Let there be a Tokenizer to translate strings to sequences of integers ("tokens") and back
uchars = sorted(set(''.join(docs))) # unique characters in the dataset become token ids 0..n-1
BOS = len(uchars) # token id for a special Beginning of Sequence (BOS) token
vocab_size = len(uchars) + 1 # total number of unique tokens, +1 is for BOS
print(f"vocab size: {vocab_size}")
자세한 설명
컴퓨터는 글자보다 숫자를 다루기 쉬워서, 글자들을 번호로 바꾸는 단계야.
1) 데이터에 등장하는 모든 글자 모으기
- ''.join(docs) : docs(이름 리스트)를 전부 이어붙여서 긴 문자열로 만듦
- set(...) : 중복 제거 → “유니크 문자들만”
- sorted(...) : 정렬 → 항상 같은 순서로 토큰 id가 정해지게 함
결과: uchars는 예를 들어 ['a','b','c',...,'z'] 같은 리스트가 된다.
2) BOS 토큰
- BOS = len(uchars)
uchars의 마지막 인덱스 다음 번호를 특수 토큰으로 쓰는 거야. - 여기서는 BOS가 “Beginning of Sequence”라고 써 있지만 실제로는
- 시작 표시로도 쓰고
- 끝 표시로도 쓰는(종료 신호) 역할을 같이 한다.
3) vocab_size
- vocab_size = len(uchars) + 1
글자 종류 수 + BOS 1개
✅ 핵심 포인트
- uchars: 데이터에 나온 글자 사전(vocabulary).
- BOS: 시작/끝을 알려주는 특수 토큰(종료 신호).
- vocab_size: 모델이 선택할 수 있는 다음 글자 후보 개수.
Step 3) Autograd(자동미분) 핵심: Value 클래스
# Let there be Autograd to recursively apply the chain rule through a computation graph
class Value:
__slots__ = ('data', 'grad', '_children', '_local_grads') # Python optimization for memory usage
def __init__(self, data, children=(), local_grads=()):
self.data = data # scalar value of this node calculated during forward pass
self.grad = 0 # derivative of the loss w.r.t. this node, calculated in backward pass
self._children = children # children of this node in the computation graph
self._local_grads = local_grads # local derivative of this node w.r.t. its children
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))
def __pow__(self, other): return Value(self.data**other, (self,), (other * self.data**(other-1),))
def log(self): return Value(math.log(self.data), (self,), (1/self.data,))
def exp(self): return Value(math.exp(self.data), (self,), (math.exp(self.data),))
def relu(self): return Value(max(0, self.data), (self,), (float(self.data > 0),))
def __neg__(self): return self * -1
def __radd__(self, other): return self + other
def __sub__(self, other): return self + (-other)
def __rsub__(self, other): return other + (-self)
def __rmul__(self, other): return self * other
def __truediv__(self, other): return self * other**-1
def __rtruediv__(self, other): return other * self**-1
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
자세한 설명
이 코드는 “딥러닝 프레임워크(예: PyTorch)가 해주는 핵심”을 직접 구현한 부분이야.
한 문장으로: 실수(손실)를 줄이기 위해, 어떤 숫자(가중치)를 얼마나 바꿔야 하는지 자동으로 계산하는 장치.
1) Value가 들고 있는 것들
- data: 실제 값(숫자 1개)
- grad: 손실(loss)을 기준으로 한 기울기(미분값)
- “이 숫자를 조금 올리면 손실이 얼마나 변해?” 같은 정보
- _children: 이 값이 어떤 값들(Value)로부터 계산됐는지(연결)
- _local_grads: “내가 자식에게 얼마나 영향을 받는지”의 로컬 미분값
- 예: z = x + y면 dz/dx = 1, dz/dy = 1
- 예: z = x * y면 dz/dx = y, dz/dy = x
2) 연산자 오버로딩(+, *)
- __add__, __mul__는 Value + Value 같은 계산을 하면
- 결과도 Value로 만들고
- 결과가 어디서 왔는지(children)
- 로컬 미분값(local_grads)
을 저장해둔다.
- 그래서 계산을 많이 하면 **계산 그래프(연결 구조)**가 자동으로 쌓인다.
3) exp, log, relu 등
- log(), exp()도 마찬가지로
- 값 계산 + 로컬 미분값 저장
- relu()는 max(0,x)라서
- x가 0보다 크면 기울기 1
- 아니면 기울기 0
4) backward()가 하는 일 (진짜 핵심)
- 그래프를 “뒤에서부터 계산 가능한 순서”로 정렬(topological order)한다.
- 손실(loss)의 grad는 1로 둔다. (∂loss/∂loss = 1)
- 뒤에서 앞으로(역순) 이동하면서
- child.grad += local_grad * v.grad
- 이게 체인룰(미분의 곱셈)이다.
초등학생 비유로는:
- 문제를 틀렸을 때 “어느 단계가 얼마나 틀렸는지”를 거꾸로 추적해서
- “여기 조금 고치면 점수가 좋아져!”를 알려주는 것.
✅ 핵심 포인트
- Value는 **값(data)**과 **기울기(grad)**를 같이 들고 다닌다.
- 연산할 때마다 “어떻게 계산됐는지”가 그래프로 쌓인다.
- backward()는 그래프를 거꾸로 돌면서 체인룰로 grad를 전파한다.
Step 4) 모델 파라미터(가중치) 초기화: state_dict + params
# Initialize the parameters, to store the knowledge of the model
n_layer = 1 # depth of the transformer neural network (number of layers)
n_embd = 16 # width of the network (embedding dimension)
block_size = 16 # maximum context length of the attention window (note: the longest name is 15 characters)
n_head = 4 # number of attention heads
head_dim = n_embd // n_head # derived dimension of each head
matrix = lambda nout, nin, std=0.08: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]
state_dict = {'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd), 'lm_head': matrix(vocab_size, n_embd)}
for i in range(n_layer):
state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd)
state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd)
params = [p for mat in state_dict.values() for row in mat for p in row] # flatten params into a single list[Value]
print(f"num params: {len(params)}")
자세한 설명
여긴 “모델의 뇌(기억)”를 만드는 부분이야.
딥러닝 모델은 **숫자 행렬(표)**을 엄청 많이 들고 있고, 학습하면서 그 숫자들이 바뀌어 지식이 된다.
1) 하이퍼파라미터(모델 크기 설정)
- n_layer = 1: 트랜스포머 블록 1개만 쓴다(아주 작은 GPT).
- n_embd = 16: 글자 하나를 16개 숫자로 표현한다(임베딩 차원).
- block_size = 16: 최대 16글자까지(문맥 길이).
- n_head = 4: 어텐션 헤드 4개.
- head_dim = 16 // 4 = 4: 한 헤드는 4차원씩 담당.
2) matrix 함수(가중치 만들기)
- matrix(nout, nin)은 nout x nin 크기의 행렬을 만든다.
- 각 원소는 Value(random.gauss(0,std)):
- 평균 0인 정규분포에서 랜덤으로 뽑아서 시작
- “처음엔 아무것도 모르는 뇌”를 랜덤으로 세팅하는 느낌
3) state_dict 안에 뭐가 들어가나(중요)
- wte: (vocab_size x n_embd)
토큰(글자 id) → 임베딩 벡터(길이 16) - wpe: (block_size x n_embd)
위치(pos id) → 임베딩 벡터(길이 16) - lm_head: (vocab_size x n_embd)
마지막 hidden(길이 16) → 다음 토큰 점수표(길이 vocab_size)
레이어마다:
- attn_wq, attn_wk, attn_wv, attn_wo: 전부 (n_embd x n_embd) = (16 x 16)
- q/k/v 만들고, 어텐션 출력도 다시 16차원으로 투영
- mlp_fc1: (4*n_embd x n_embd) = (64 x 16)
- mlp_fc2: (n_embd x 4*n_embd) = (16 x 64)
4) params로 납작하게 만들기
- optimizer(Adam)가 업데이트하려면 “모든 파라미터를 한 줄로” 돌기 편해.
- 그래서 params는 state_dict 안의 모든 Value를 전부 꺼내서 1차원 리스트로 만든 것.
✅ 핵심 포인트
- state_dict는 모델이 학습하는 모든 가중치(지식 저장소).
- 크기(차원)가 아주 작게 설정되어 있어서 원리를 보기 좋게 만든 micro GPT.
- params는 optimizer가 업데이트할 전체 파라미터 리스트.
Step 5) 모델 아키텍처: linear / softmax / rmsnorm / gpt
# Define the model architecture: a function mapping tokens and parameters to logits over what comes next
# Follow GPT-2, blessed among the GPTs, with minor differences: layernorm -> rmsnorm, no biases, GeLU -> ReLU
def linear(x, w):
return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]
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]
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]
def gpt(token_id, pos_id, keys, values):
tok_emb = state_dict['wte'][token_id] # token embedding
pos_emb = state_dict['wpe'][pos_id] # position embedding
x = [t + p for t, p in zip(tok_emb, pos_emb)] # joint token and position embedding
x = rmsnorm(x) # note: not redundant due to backward pass via the residual connection
for li in range(n_layer):
# 1) Multi-head Attention block
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)
x_attn = []
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]]
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)
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)
x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])
x = [a + b for a, b in zip(x, x_residual)]
# 2) MLP block
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 = [a + b for a, b in zip(x, x_residual)]
logits = linear(x, state_dict['lm_head'])
return logits
자세한 설명
A) linear(x, w)
- 입력 x는 벡터(길이 nin)
- w는 행렬(크기 nout x nin)
- 출력은 벡터(길이 nout)
코드에서:
- for wo in w는 행렬의 “각 행(row)”를 돈다.
- sum(wi * xi ...)는 내적(dot product): 한 행과 x를 곱해 점수 1개를 만든다.
- 결과적으로 “선형변환(Linear layer)”이 된다.
B) softmax(logits)
소프트맥스는 “점수표(logits)”를 “확률표(합이 1)”로 바꿔.
- max_val = max(val.data ...): 가장 큰 값을 구함
- (val - max_val)을 하는 이유:
- exp는 큰 숫자에서 폭발하기 쉬움(오버플로우)
- 그래서 최대값을 빼서 안정적으로 만든다.
- exps = ... exp(): 각 점수를 exp로 바꿔서 양수로 만들고
- total = sum(exps): 전체 합
- e/total: 확률로 정규화
중요: 여기서 exp/log/나눗셈이 다 Value 연산이라서 계산 그래프가 연결된다.
즉 softmax도 학습에 포함돼.
C) rmsnorm(x)
RMSNorm은 “벡터 크기(스케일)를 적당히 맞춰서 학습을 안정적으로 만드는 정규화”야.
- ms = sum(xi * xi) / len(x) : 평균 제곱(mean square)
- scale = (ms + 1e-5) ** -0.5 :
- 1/sqrt(ms + eps)랑 같은 의미
- eps(1e-5)는 0으로 나누는 걸 방지
- [xi * scale for xi in x]: 벡터를 스케일 조정해서 크기를 일정하게 만든다.
D) gpt(...) : “한 위치(pos)에서 다음 토큰 점수표(logits) 만들기”
1) 입력은 무엇?
- token_id: 현재 글자(토큰) id
- pos_id: 지금이 몇 번째 위치인지(0,1,2,...)
- keys, values: 어텐션용 “기억장부”
- 지금까지 계산한 k/v를 쌓아두는 캐시
2) 임베딩 합치기
- tok_emb = wte[token_id] → 길이 16
- pos_emb = wpe[pos_id] → 길이 16
- x = tok_emb + pos_emb → 길이 16
3) (레이어마다) 어텐션 블록
- x_residual = x: 잔차 연결(residual connection)을 위해 저장
- x = rmsnorm(x): 정규화
- q,k,v = linear(x, Wq/Wk/Wv):
- 각각 길이 16 벡터가 나옴
- keys[li].append(k), values[li].append(v):
- 지금 위치의 k/v를 “기억장부에 추가”
- 그래서 다음 위치 계산할 때 과거를 볼 수 있다.
멀티헤드 어텐션 내부
- 헤드 4개로 나누기:
- 각 헤드는 4차원(head_dim=4)
- q_h, k_h, v_h는 해당 헤드 부분만 잘라온 것
- attn_logits:
- 과거의 각 시점 t에 대해
- dot(q_h, k_h[t]) / sqrt(head_dim)
- “현재 질문(q)이 과거의 어떤 키(k)와 비슷한가?”
- attn_weights = softmax(attn_logits):
- 중요도(가중치) 확률
- head_out:
- 그 가중치로 과거의 v를 섞어서 새로운 벡터를 만든다.
- 모든 헤드 결과를 이어붙여 x_attn(길이 16) 완성
- x = linear(x_attn, Wo)로 다시 16차원으로 정리
- x = x + x_residual로 잔차 연결
👉 “과거에서 필요한 정보만 골라 섞어오는 손전등”이라고 생각하면 돼.
4) MLP 블록
- 다시 residual 저장 → norm → fc1 → relu → fc2 → residual 더하기
- fc1은 16 → 64로 늘리고
- fc2는 64 → 16으로 줄여
- 목적: 어텐션 결과를 한 번 더 “깊게 계산”하는 작은 뇌
5) lm_head로 logits 만들기
- logits = linear(x, lm_head) → 길이 vocab_size
- 이 logits는 “다음 글자가 될 후보들”에 대한 점수표야.
✅ 핵심 포인트
- linear: 행렬×벡터 = 신경망의 기본 연산.
- softmax: 점수 → 확률(학습 가능한 계산 그래프 포함).
- rmsnorm: 학습 안정화(크기 맞추기).
- gpt: (임베딩) → (어텐션+MLP) → (다음 토큰 logits).
- keys/values 캐시 덕분에 미래를 안 보고 과거만 보게 된다(순서대로 append하니까).
Step 6) Adam 옵티마이저 준비: 하이퍼파라미터 + m, v 버퍼
# Let there be Adam, the blessed optimizer and its buffers
learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
m = [0.0] * len(params) # first moment buffer
v = [0.0] * len(params) # second moment buffer
자세한 설명
Adam은 “경사하강법(gradient descent)”의 업그레이드 버전이야.
- learning_rate: 한 번에 얼마나 움직일지(수정량 크기)
- beta1: m의 이동평균 비율(기울기 평균을 얼마나 오래 기억할지)
- beta2: v의 이동평균 비율(기울기 제곱 평균을 얼마나 오래 기억할지)
- eps_adam: 0으로 나눔 방지 + 안정성
버퍼:
- m[i]: i번째 파라미터의 기울기 평균(방향 기억)
- v[i]: i번째 파라미터의 기울기 제곱 평균(크기 기억)
더 자세한 비유로
- m은 “최근에 어떤 방향으로 틀렸는지” 기록하는 노트
- v는 “얼마나 크게 틀렸는지” 기록하는 노트
✅ 핵심 포인트
- Adam은 **방향(m)**과 **크기(v)**를 기억해서 더 안정적으로 업데이트한다.
- m, v는 파라미터 개수만큼 각각 하나씩 가진다.
Step 7) 학습 루프: 토큰화 → forward → loss → backward → Adam 업데이트
# Repeat in sequence
num_steps = 1000 # number of training steps
for step in range(num_steps):
# Take single document, tokenize it, surround it with BOS special token on both sides
doc = docs[step % len(docs)]
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
n = min(block_size, len(tokens) - 1)
# Forward the token sequence through the model, building up the computation graph all the way to the loss
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) # final average loss over the document sequence. May yours be low.
# Backward the loss, calculating the gradients with respect to all model parameters
loss.backward()
# Adam optimizer update: update the model parameters based on the corresponding gradients
lr_t = learning_rate * (1 - step / num_steps) # linear learning rate decay
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
print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}", end='\r')
자세한 설명
A) 매 스텝에서 학습할 문서(이름) 하나 고르기
- doc = docs[step % len(docs)]
- step이 커져도 docs 범위를 넘지 않도록 반복해서 뽑는다.
B) 토큰화 + BOS로 감싸기
- tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
- 예: doc="emma"라면 [BOS, e, m, m, a, BOS](각각은 숫자 id)
- 여기서 uchars.index(ch)는 ch가 uchars에서 몇 번째인지 찾는 것.
- 느리지만(선형 탐색) “가장 단순한 구현”을 보여주려는 코드라 이렇게 했다.
C) n (이번 문서에서 학습할 길이)
- len(tokens) - 1인 이유:
- 우리는 pos_id 위치의 입력으로 pos_id+1 위치의 정답을 맞히니까
- 마지막 토큰은 “다음 정답”이 없다.
- min(block_size, ...):
- 문서가 길면 최대 16글자까지만 보게 자른다.
D) forward: 손실(loss) 만들기
- keys, values를 레이어별로 빈 리스트로 시작
- 순서대로 pos를 증가시키며 append하니까 “과거만 저장”된다.
- 각 위치 pos_id에서:
- token_id = 현재 입력
- target_id = 다음 정답
- logits = gpt(...)
- probs = softmax(logits)
- loss_t = -probs[target_id].log()
loss_t가 의미하는 것
- 정답 확률이 1에 가까우면 -log(1) ≈ 0 (벌점 거의 없음)
- 정답 확률이 0.01이면 -log(0.01)은 큼 (벌점 큼)
즉 정답을 높게 주도록 학습시키는 공식. - loss = (1/n) * sum(losses)로 평균 손실
E) backward: 기울기 계산
- loss.backward() 호출 한 번으로
- loss를 만드는 데 관여했던 모든 Value들의 grad가 채워진다.
- 여기엔 모델의 모든 파라미터도 포함된다.
F) Adam 업데이트
- lr_t = learning_rate * (1 - step/num_steps):
- 학습 초반엔 크게 움직이고, 후반엔 조금씩 움직이게 만드는 “선형 감소”.
- m, v 업데이트:
- m은 grad 평균, v는 grad^2 평균
- m_hat, v_hat:
- 초반에는 m, v가 0에서 시작해서 편향(bias)이 생기니까 보정해줌.
- p.data -= ...:
- 파라미터 실제 값을 업데이트(학습)
- p.grad = 0:
- 다음 스텝을 위해 기울기 초기화(안 하면 누적돼서 망함)
G) 출력
- end='\r'는 같은 줄에서 계속 덮어써서 진행 상황처럼 보이게 해준다.
✅ 핵심 포인트
- 학습은 “다음 글자 맞히기” 게임이다.
- loss_t = -log(p(정답)) → 정답 확률을 높이도록 강제.
- loss.backward()가 모든 파라미터의 grad를 채운다.
- Adam이 p.data를 업데이트하고 p.grad는 0으로 리셋한다.
Step 8) Inference(생성): temperature로 샘플링해서 새 이름 20개 생성
# Inference: may the model babble back to us
temperature = 0.5 # in (0, 1], control the "creativity" of generated text, low to high
print("\n--- inference (new, hallucinated names) ---")
for sample_idx in range(20):
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
token_id = BOS
sample = []
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])
print(f"sample {sample_idx+1:2d}: {''.join(sample)}")
자세한 설명
학습이 끝났으니 이제 모델이 “새 이름”을 만들어내는 단계야.
A) temperature(창의력 조절)
- logits를 temperature로 나누고 softmax를 한다.
- temperature가 낮으면:
- logits 차이가 더 커짐 → softmax가 “한쪽으로 쏠림”
- 결과: 무난하고 비슷한 이름이 더 많이 나옴
- temperature가 높으면(1에 가까울수록):
- 확률이 더 고르게 됨
- 결과: 더 다양한 이름이 나오지만 이상한 것도 늘 수 있음
B) 생성 과정(한 글자씩)
- token_id = BOS에서 시작
- 매 위치(pos_id)마다:
- logits = gpt(...)
- 확률로 바꾼 뒤
- random.choices로 확률에 따라 한 토큰 뽑기
- 뽑힌 토큰이 BOS면 “끝!” (이름 생성 종료)
- 아니면 uchars[token_id]로 문자로 바꿔서 sample에 추가
C) 캐시(keys/values)를 왜 다시 빈 걸로 시작하나?
- 이름 하나를 만들 때는 그 이름 내부의 문맥만 쓰면 되니까
- 새 샘플 시작마다 캐시를 초기화해야 “이전 샘플의 기억”이 섞이지 않아.
✅ 핵심 포인트
- 생성은 BOS에서 시작해서 한 글자씩 확률로 뽑는 과정.
- temperature는 다양성(창의력) 조절 다이얼.
- 샘플마다 keys/values 캐시를 초기화한다.
'AI' 카테고리의 다른 글
| Microgpt 분석 #1 (0) | 2026.02.20 |
|---|---|
| AI 에이전트 오케스트레이션 바닥부터 #1 (0) | 2026.02.17 |
| WebMCP Starter webmcp-starter 정리: “Midnight Eats”로 보는 에이전트 친화적인 웹 주문 흐름 (0) | 2026.02.14 |
| Excalidraw MCP 서버 아키텍처 가이드 (0) | 2026.02.12 |
| UI/UX Pro Max 검색 시스템 분석 문서 (0) | 2026.02.07 |
