1. 인공 신경망: 생물학적 뉴런에서 수학 함수로
1.1 신경망의 최소 단위
인공 신경망(Neural Network)은 뇌의 뉴런이 동작하는 방식에서 영감을 받았습니다. 단일 뉴런은 본질적으로 다음과 같은 수학 함수입니다:
output = activation(w · x + b) 여기서 x는 입력 벡터, w는 가중치, b는 편향, activation은 활성화 함수(주로 사용되는 것은 Sigmoid, ReLU, GELU)입니다. 이런 "뉴런"을 수천 개씩 층으로 쌓고 서로 연결하면, 현대 딥러닝의 토대가 만들어집니다.
1.2 신경망 훈련의 3대 요소
- 순전파(Forward Propagation): 데이터가 입력층에서 출력층으로 흐르며 예측 결과를 생성합니다.
- 손실 함수(Loss Function): 예측과 정답 사이의 차이를 측정합니다. 분류 작업에는 교차 엔트로피, 회귀 작업에는 평균 제곱 오차가 일반적입니다.
- 역전파 + 경사 하강법(Backpropagation + Gradient Descent): 출력층에서 입력층 방향으로 각 파라미터의 손실에 대한 "기여도"(그래디언트)를 계산하고, 그래디언트의 반대 방향으로 가중치를 미세 조정해 다음 예측을 더 정확하게 만듭니다.
💡 핵심 직관: 신경망 훈련이란 본질적으로 "수십억 차원의 parameter space에서 손실 함수의 값을 최소화하는 가중치 조합을 찾는" 작업입니다. 그래서 LLM에는 엄청난 양의 데이터와 컴퓨팅 자원이 필요합니다 — parameter space가 너무 거대하기 때문입니다.
2. Transformer: 현대 LLM의 초석
2017년 Google이 Attention Is All You Need라는 논문에서 Transformer 아키텍처를 제안하며 AI의 발전 궤적을 완전히 바꾸어 놓았습니다. 핵심 혁신은 순환 구조를 완전히 버리고 어텐션 메커니즘(Attention Mechanism)만으로 시퀀스 데이터를 처리한다는 점입니다.
2.1 왜 Transformer가 NLP를 평정했는가
| 특성 | RNN / LSTM | Transformer |
|---|---|---|
| 장거리 의존성 | 초기 컨텍스트를 잊어버리기 쉬움 | 임의의 위치끼리 직접 연결 |
| 병렬 계산 | 반드시 순차 처리 | 모든 위치를 동시에 처리 |
| 훈련 속도 | 느림 | 빠름(GPU를 충분히 활용) |
| 확장성 | 1000억 파라미터 이상으로 확장하기 어려움 | 1000억 파라미터 이상을 네이티브로 지원 |
2.2 셀프 어텐션(Self-Attention)
셀프 어텐션의 핵심 아이디어는, 시퀀스 안의 모든 단어에 대해 다른 모든 단어를 "살펴보고" 각 단어가 자신에게 얼마나 중요한지 판단하는 것입니다.
예를 들어 다음 문장을 봅시다 — "철수는 어제 서울로 출장을 갔고, 오늘 그는 돌아왔다":
- 각 단어는 세 개의 벡터를 만듭니다: Query(Q, 질의 벡터), Key(K, 키 벡터), Value(V, 값 벡터).
- "그"에 대해, 그 Q와 다른 모든 단어의 K의 내적을 계산해 관련도 점수를 얻습니다.
- 점수에 Softmax 정규화를 적용해 어텐션 가중치를 얻습니다(예: "그"의 "철수"에 대한 가중치는 0.8, "서울"에 대한 가중치는 0.1).
- 이 가중치를 사용해 모든 단어의 V를 가중 합산해, "그"라는 단어의 컨텍스트가 풍부한 새 표현을 얻습니다.
수식은 다음과 같이 간결하게 쓸 수 있습니다:
Attention(Q, K, V) = softmax(Q · K^T / √d_k) · V 여기서 d_k는 Key 벡터의 차원이고, 그 제곱근으로 나누는 이유는 내적이 너무 커져서 Softmax의 그래디언트가 소실되는 것을 막기 위해서입니다.
2.3 멀티 헤드 어텐션(Multi-Head Attention)
멀티 헤드 어텐션은 셀프 어텐션의 업그레이드 버전입니다. 모델이 서로 다른 "표현 부분공간"에서 여러 셀프 어텐션을 병렬로 수행하고, 그 결과를 이어 붙입니다. 이를 통해 모델은 여러 종류의 관계를 동시에 포착할 수 있습니다:
- 어떤 헤드는 통사적 관계(주어-서술어-목적어)에 집중할 수 있습니다.
- 다른 헤드는 의미적 관계(동의어, 반의어)를 살필 수 있습니다.
- 세 번째 헤드는 장거리 지시 대상(대명사가 어떤 명사를 가리키는지)을 다룰 수 있습니다.
2.4 위치 인코딩(Positional Encoding)
Transformer는 모든 단어를 병렬로 처리하기 때문에 본질적으로 "순서" 정보를 잃습니다. 예를 들어 "개가 사람을 물었다"와 "사람이 개를 물었다"는 글자 그대로의 토큰은 완전히 같고 순서만 다릅니다. 위치 인코딩은 각 위치의 단어 임베딩에 고유한 "위치 벡터"를 더해 모델이 각 단어의 위치를 알 수 있게 합니다.
최초의 Transformer는 고정된 사인/코사인 함수로 위치 인코딩을 생성했습니다. 이후의 모델(LLaMA, Qwen 등)은 RoPE(Rotary Position Embeddings, 회전 위치 인코딩)를 채택해 더 긴 시퀀스를 더 잘 처리합니다.
3. Transformer의 전체 아키텍처
표준 Transformer 블록은 두 부분으로 구성됩니다:
입력 → [LayerNorm → 멀티 헤드 어텐션 → 잔차 연결]
→ [LayerNorm → 피드포워드 네트워크(FFN) → 잔차 연결]
→ 출력 3.1 Transformer 전체 아키텍처 다이어그램(한눈에 파악)
핵심 3종 세트:
- 멀티 헤드 어텐션 계층:관계를 찾는 역할 — 모든 단어가 다른 모든 단어를 볼 수 있도록 한다(Decoder는 Causal Mask로 과거 단어만 본다).
- 피드포워드 네트워크(FFN):정보를 가공하는 역할 — 각 단어에 개별적으로 비선형 변환을 적용해 고차원의 특징을 추출한다.
- 잔차 연결 + LayerNorm: 이전 층의 입력을 직접 출력에 더해 그래디언트 소실을 막고, 깊은 네트워크에서도 훈련이 가능하게 한다.
GPT 계열 모델(ChatGPT, DeepSeek, Qwen 등)은 이런 블록을 수십~수백 층 쌓아 만든 Decoder-only Transformer입니다.
4. 사전학습, SFT, RLHF: 대규모 모델의 "3단계 훈련법"
"말할 수 있는" 상태에서 "들을 수 있는", 그리고 "사랑받는" 상태가 되기까지, 대규모 모델은 세 단계를 거칩니다:
4.0 3단계 훈련법의 흐름도
3단계는 점진적으로 쌓아 올린다: "세계를 이해" → "사람의 말을 이해" → "인간 가치관에 정렬". 어느 한 단계라도 빠지면 모델은 "쓸모가 없다".
4.1 사전학습(Pre-Training)
방대한 인터넷 텍스트(수조 토큰)로 모델을 훈련해 "다음 단어를 예측"하는 법을 가르칩니다. 이 단계가 가장 오래 걸리고 비용도 가장 큽니다 — DeepSeek-V3는 14.8조 토큰 훈련에 약 557만 달러(GPU 임대료)를 썼습니다.
4.2 지도 미세조정(Supervised Fine-Tuning, SFT)
사람이 라벨링한 고품질 "지시-응답" 쌍으로 모델을 훈련해 "사람의 지시를 듣고 그에 맞게 답하는" 법을 가르칩니다. 이 단계의 데이터 양은 사전학습보다 훨씬 적지만(수만~수십만 건) 품질 요구 수준은 극도로 높습니다.
4.3 인간 피드백 기반 강화학습(RLHF)
사람이 모델이 만든 여러 응답에 순위를 매기고, 그 순위 데이터로 "보상 모델"(Reward Model)을 훈련합니다. 그다음 강화학습(주로 PPO 또는 DPO 알고리즘)으로 모델이 더 선호되는 응답을 생성하도록 조정합니다.
5. 왜 GPU 클러스터가 필요한가?
1000억 파라미터 모델을 훈련하려면, 순전파 + 역전파 한 번에 다음이 필요합니다:
- 입력 토큰의 임베딩(파라미터 수 × 시퀀스 길이)
- 어텐션 행렬(시퀀스 길이² × 어텐션 헤드 수)
- 그래디언트(파라미터 수)
- 옵티마이저 상태(모멘텀, 분산으로 3-4배 파라미터 수)
단일 GPU의 메모리(80GB)에는 도저히 들어가지 않으므로, 여러 병렬 전략을 조합해야 합니다:
| 병렬 방식 | 핵심 아이디어 | 대표 프레임워크 |
|---|---|---|
| 데이터 병렬 | 데이터를 여러 GPU에 분할, 각 GPU가 모델 전체 실행 | PyTorch DDP |
| 모델 병렬 | 모델의 서로 다른 층을 서로 다른 GPU에 배치 | Megatron-LM |
| 파이프라인 병렬 | 모델을 단계로 나누고, 각 GPU가 서로 다른 마이크로 배치를 처리 | GPipe, PipeDream |
| 텐서 병렬 | 단일 행렬 연산을 여러 GPU에 분할 | Megatron-LM |
6. LLM에서 Agent로: 능력의 도약
LLM이 충분히 강해지자 사람들은 여기에 "확장 기능"을 붙이기 시작했습니다: 모델이 "말하는" 것만이 아니라 "행동하는" 것까지 가능하도록. 이것이 AI Agent의 출발점입니다:
- ReAct(Reason + Act, 추론 + 행동): 모델이 먼저 무엇을 할지 사고한 다음, 어떤 도구를 호출할지 결정합니다.
- Function Calling(함수 호출): 모델이 구조화된 "도구 호출 요청"을 출력하고, 외부 시스템이 그것을 실행합니다.
- 멀티 Agent 협업(Multi-Agent Collaboration): 여러 Agent가 서로 다른 역할을 맡아 협력해 작업을 완수합니다.
이 부분에 대해서는 다음 장 "Agent 진화"에서 깊이 있게 다룹니다.
7. 이 장의 요약
- ✅ Transformer는 "어텐션 메커니즘"으로 순환 구조를 대체해 병렬 훈련과 장거리 의존 모델링을 실현했다.
- ✅ 셀프 어텐션으로 각 단어가 다른 모든 단어를 "볼" 수 있고, 멀티 헤드 메커니즘으로 여러 관계를 동시에 포착할 수 있다.
- ✅ 사전학습 + SFT + RLHF는 LLM을 훈련하는 세 가지 정석이다.
- ✅ 1000억 파라미터 모델은 GPU 클러스터와 다양한 병렬 전략이 필수적이다.
8. 핸즈온: Mini Transformer를 처음부터 구축하기
이론을 모두 다뤘으니, 이번 섹션에서는 PyTorch로 6층 Decoder-only Transformer를 처음부터 만들어 작은 말뭉치에서 훈련과 추론을 시연합니다. 전체 코드는 약 250줄, CPU/GPU에서 모두 동작하며, Transformer의 톱니바퀴 하나하나가 눈에 보이게 만드는 것이 목표입니다.
8.1 목표와 환경
- 6층 Decoder-only Transformer(약 1.2M 파라미터) 구현.
- ~500행 정도의 초소형 말뭉치에서 문자 단위 언어 모델을 훈련하고 loss 감소를 관찰.
- 실행 가능한 스크립트 두 개 제공:
train.py와sample.py.
환경 설정(2줄):
pip install torch numpy
python -c "import torch; print('torch', torch.__version__)" 8.2 데이터 준비
짧은 "한시 스타일" 더미 데이터를 사용합니다(자신만의 input.txt로 교체해도 좋습니다):
cat > data/input.txt << 'EOF'
밤의 생각
침상 앞에 밝은 달빛, 의심하건대 이는 땅 위의 서리.
머리 들면 산의 달을 바라보고, 고개 숙이면 고향을 생각하네.
봄 새벽
봄잠은 새벽을 모르니,处处에서 울새 소리를 듣네.
어젯밤 풍우 소리, 꽃이 떨어진 줄 몇이나 알랴.
백구작루
해는 산에 기대어 다하고, 황하는 바다로 흘러가네.
千里의 눈을 다하려거든, 한 층 더 올라갈지니.
EOF
wc -c data/input.txt 문자 단위 언어 모델은 파일 전체를 하나의 긴 문자열로 읽으면 됩니다, tokenizer는 필요 없습니다.
8.3 전체 코드: train.py
"""Mini Transformer: 6층 Decoder-only, 문자 단위 언어 모델.
사용법:
python train.py
"""
from pathlib import Path
# ---------- 1. 데이터 로더 ----------
class CharDataset:
"""텍스트 파일을 (block_size) 길이의 학습 샘플로 자른다."""
def __init__(self, path: str, block_size: int = 64):
self.text = Path(path).read_text(encoding="utf-8")
self.chars = sorted(set(self.text))
self.vocab_size = len(self.chars)
self.stoi = {c: i for i, c in enumerate(self.chars)}
self.itos = {i: c for i, c in enumerate(self.chars)}
self.block_size = block_size
self.data = torch.tensor([self.stoi[c] for c in self.text], dtype=torch.long)
print(f"[data] chars={self.vocab_size} len={len(self.data)}")
def get_batch(self, batch_size: int = 32):
ix = torch.randint(0, len(self.data) - self.block_size - 1, (batch_size,))
x = torch.stack([self.data[i : i + self.block_size] for i in ix])
y = torch.stack([self.data[i + 1 : i + 1 + self.block_size] for i in ix])
return x, y
# ---------- 2. 모델 컴포넌트 ----------
class PositionalEncoding(nn.Module):
"""사인 위치 인코딩. 고정되어 학습되지 않음."""
def __init__(self, d_model: int, max_len: int = 512):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer("pe", pe.unsqueeze(0)) # (1, max_len, d_model)
def forward(self, x):
return x + self.pe[:, : x.size(1)]
class CausalSelfAttention(nn.Module):
"""멀티 헤드 인과 셀프 어텐션(Decoder에서 사용)."""
def __init__(self, d_model: int, n_heads: int, max_len: int = 512, dropout: float = 0.1):
super().__init__()
assert d_model % n_heads == 0
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model)
self.proj = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
# causal mask: 위쪽 삼각(대각선 제외)을 -inf로
mask = torch.triu(torch.ones(max_len, max_len), diagonal=1).bool()
self.register_buffer("mask", mask)
def forward(self, x):
B, T, C = x.shape
qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2] # (B, h, T, head_dim)
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
att = att.masked_fill(self.mask[:T, :T], float("-inf"))
att = F.softmax(att, dim=-1)
att = self.dropout(att)
out = (att @ v).transpose(1, 2).reshape(B, T, C)
return self.proj(out)
class FeedForward(nn.Module):
"""Transformer FFN: GELU + Linear + Dropout."""
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout),
)
def forward(self, x):
return self.net(x)
class Block(nn.Module):
"""Pre-LN Decoder 블록: Attention -> FFN, 각각 잔차 연결 포함."""
def __init__(self, d_model: int, n_heads: int, d_ff: int, max_len: int, dropout: float):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_heads, max_len, dropout)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = FeedForward(d_model, d_ff, dropout)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ffn(self.ln2(x))
return x
class MiniTransformer(nn.Module):
"""6층 Decoder-only Transformer, 문자 단위 언어 모델."""
def __init__(
self,
vocab_size: int,
d_model: int = 128,
n_heads: int = 4,
d_ff: int = 512,
n_layers: int = 6,
max_len: int = 256,
dropout: float = 0.1,
):
super().__init__()
self.tok_emb = nn.Embedding(vocab_size, d_model)
self.pos_enc = PositionalEncoding(d_model, max_len)
self.blocks = nn.ModuleList(
[Block(d_model, n_heads, d_ff, max_len, dropout) for _ in range(n_layers)]
)
self.ln_f = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, vocab_size, bias=False)
# 가중치 공유: 입력 임베딩과 출력 head가 가중치를 공유 (BERT/GPT에서 자주 쓰이는 트릭)
self.head.weight = self.tok_emb.weight
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Embedding):
nn.init.normal_(m.weight, std=0.02)
def forward(self, idx, targets=None):
x = self.tok_emb(idx)
x = self.pos_enc(x)
for blk in self.blocks:
x = blk(x)
x = self.ln_f(x)
logits = self.head(x)
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
return logits, loss
@torch.no_grad()
def generate(self, idx, max_new_tokens: int = 100, temperature: float = 1.0, top_k: int = 20):
"""자기회귀 생성. temperature + top-k 샘플링 포함."""
for _ in range(max_new_tokens):
idx_cond = idx[:, -self.pos_enc.pe.size(1) :]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature
if top_k:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = F.softmax(logits, dim=-1)
nxt = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, nxt], dim=1)
return idx
# ---------- 3. 훈련 루프 ----------
def main():
torch.manual_seed(0)
ds = CharDataset("data/input.txt", block_size=64)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = MiniTransformer(vocab_size=ds.vocab_size).to(device)
print(f"[model] params={sum(p.numel() for p in model.parameters()) / 1e6:.2f}M device={device}")
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
steps = 1000
batch_size = 32
t0 = time.time()
for step in range(steps):
x, y = ds.get_batch(batch_size)
x, y = x.to(device), y.to(device)
_, loss = model(x, y)
opt.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
if step % 100 == 0 or step == steps - 1:
dt = time.time() - t0
print(f"step {step:4d} loss={loss.item():.4f} elapsed={dt:.1f}s")
torch.save({"model": model.state_dict(), "stoi": ds.stoi, "itos": ds.itos}, "ckpt.pt")
print("[done] saved ckpt.pt")
if __name__ == "__main__":
main() 8.4 추론 스크립트: sample.py
"""훈련된 ckpt.pt로 텍스트를 생성한다."""
from train import MiniTransformer
ckpt = torch.load("ckpt.pt", map_location="cpu")
stoi, itos = ckpt["stoi"], ckpt["itos"]
model = MiniTransformer(vocab_size=len(itos))
model.load_state_dict(ckpt["model"])
model.eval()
prompt = "밤의 생각\n"
idx = torch.tensor([[stoi[c] for c in prompt]], dtype=torch.long)
out = model.generate(idx, max_new_tokens=120, temperature=0.8, top_k=10)[0].tolist()
print("=== generated ===")
print("".join(itos[i] for i in out)) 8.5 실행과 예상 출력
mkdir -p data
# (8.2절의 input.txt를 작성)
python train.py 예상 출력(CPU에서 약 30-60초, MPS/GPU에서는 수 초):
[data] chars=28 len=87
[model] params=1.21M device=cpu
step 0 loss=3.3321 elapsed=0.0s
step 100 loss=2.7102 elapsed=2.1s
step 200 loss=2.3504 elapsed=4.0s
...
step 900 loss=1.8421 elapsed=32.5s
step 999 loss=1.7903 elapsed=36.0s
[done] saved ckpt.pt
python sample.py
=== generated ===
밤의 생각
침상 앞에 밝은 달빛, 의심하건대 이는 땅 위의 서리.
머리 들면 산의 달을 바라보고, 고개 숙이면 고향을 생각하네.
봄잠은 새벽을 모르니,处处에서 울새 소리를 듣네. 스크린샷: 훈련 곡선(loss vs step) — tensorboard 또는 matplotlib으로 loss.item()을 그려 loss_curve.png로 저장. 생성 샘플 — 터미널 텍스트를 그대로 복사.
8.6 튜닝 팁
- loss가 떨어지지 않음: 데이터가 너무 짧음 → 더 긴
input.txt(예: 《全唐诗》) 사용; 학습률을 1e-4 / 5e-4로 시도. - 생성이 한 글자로 붕괴: temperature가 너무 낮음 — 0.8-1.2로 올림; top_k를 20-50으로 올림.
- 메모리 부족:
batch_size를 8로 낮추거나torch.amp.autocast로 반정밀도 실행. - 더 빠르게:
d_model=96,n_layers=4로 두면 파라미터가 ~400K로 줄어듦.
8.7 다음 단계
- 완전한 GPT:
top-p (nucleus)샘플링 추가, 추론 가속을 위해 KV-cache 도입. - LLaMA 아키텍처: LayerNorm을
RMSNorm으로, 사인 위치 인코딩을RoPE로, GELU FFN을SwiGLU로 바꾸고,GQA를 더해 메모리를 줄임. - 멀티 GPU 훈련:
torchrun --nproc_per_node=2 train.py+DistributedDataParallel. - 진짜 tokenizer 사용:
CharDataset을tiktoken(GPT-2 BPE) 또는sentencepiece로 교체.