Transformer 블록과 언어모델

학습 목표
  • 어텐션만으로는 순서 정보가 사라진다는 것을 실험으로 확인하고, 위치 임베딩이 왜 필요한지 설명할 수 있다
  • LayerNorm을 직접 계산할 수 있고, 잔차 연결이 왜 필요한지 Ch04·Ch07과 연결해 설명할 수 있다
  • FFN의 역할과 “확장 → 축소” 구조를 설명할 수 있다
  • Transformer 블록 하나의 구조를 그리고, 파라미터 수를 직접 계산할 수 있다
  • 블록을 쌓은 것이 Transformer이고, 실제 모델의 크기를 공식으로 맞춰볼 수 있다
  • 언어모델의 목표가 다음 토큰 예측 하나임을 설명하고, 확률을 Softmax로 계산할 수 있다
  • Temperature가 확률분포를 어떻게 바꾸는지 계산하고 해석할 수 있다

Ch11에서 부품 하나 — Multi-Head Self-Attention — 을 손에 넣었다. 이 장에서는 그것을 블록으로 조립하고, 블록을 쌓아 언어모델을 만든다. 새로 배우는 계산은 LayerNorm 하나뿐이고, 나머지는 지금까지 배운 부품의 조립이다.


부족한 세 가지

Ch11의 어텐션은 강력하지만, 그것만으로는 모델이 되지 않는다. 세 가지가 비어 있다.

빠진 것 증상 이 장의 해법
순서 단어를 섞어도 결과가 같다 위치 임베딩
깊이 블록을 쌓으면 학습이 무너진다 (Ch04 기울기 소실) 잔차 연결 + LayerNorm
가공 어텐션은 섞기만 한다 FFN

하나씩 채워 나가자.


1. 순서 — 어텐션은 정말 순서를 모르는가

말로만 하지 말고 실험으로 확인하자. Ch11의 Q·K·V를 그대로 쓰되, 토큰의 순서만 바꿔서 어텐션을 계산해 본다.

코드
import numpy as np
import pandas as pd

Q = np.array([[2, 1], [1, 2], [1, 3]], dtype=float)
K = np.array([[1, 0], [0, 2], [1, 1]], dtype=float)
V = np.array([[1, 0], [0, 1], [1, 1]], dtype=float)
tokens = np.array(['고양이가', '생선을', '먹었다'])

def attention(Q, K, V):
    S = (Q @ K.T) / np.sqrt(Q.shape[1])
    e = np.exp(S - S.max(axis=1, keepdims=True))
    W = e / e.sum(axis=1, keepdims=True)
    return W @ V

out1 = attention(Q, K, V)

perm = [1, 0, 2]                       # 1번과 2번 토큰의 자리를 바꾼다
out2 = attention(Q[perm], K[perm], V[perm])

pd.DataFrame({
    '원래 순서': tokens,
    '결과': [f'({v[0]:.3f}, {v[1]:.3f})' for v in out1],
    '바꾼 순서': tokens[perm],
    '결과 ': [f'({v[0]:.3f}, {v[1]:.3f})' for v in out2],
}).to_html(index=False, border=0)
표 1: 토큰 순서를 바꿔도 각 토큰의 새 표현은 그대로다
원래 순서 결과 바꾼 순서 결과
고양이가 (0.752, 0.752) 생선을 (0.380, 0.926)
생선을 (0.380, 0.926) 고양이가 (0.752, 0.752)
먹었다 (0.214, 0.977) 먹었다 (0.214, 0.977)
결과가 자리만 바뀐 채 그대로다

“생선을”의 새 표현은 원래 순서에서든 바뀐 순서에서든 완전히 같은 값이다.

즉 어텐션에게 “고양이가 생선을 먹었다”와 “생선을 고양이가 먹었다”는 같은 문장이다. 내적에도, Softmax에도, 가중평균에도 “몇 번째”라는 정보가 들어갈 자리가 없기 때문이다.

Ch10 첫 절에서 텍스트가 이미지보다 어려운 이유로 꼽았던 “순서가 의미를 바꾼다”가 여기서 정면으로 되돌아온 것이다. RNN은 순서를 구조(한 칸씩 처리)로 갖고 있었지만, Transformer는 병렬 처리를 얻는 대가로 그것을 잃었다.

해법: 위치를 입력에 더해 준다

구조에 없다면 데이터에 넣으면 된다. 각 위치마다 고유한 벡터를 만들어 토큰 임베딩에 더한다.

정의: 위치 임베딩 (Positional Embedding)

위치 번호 \(0, 1, 2, \ldots\) 각각에 대응하는 \(d\)차원 벡터를 학습으로 만들고, 같은 자리의 토큰 임베딩에 더해서 입력으로 쓴다.

\[\text{입력}_t = \text{토큰 임베딩}(x_t) + \text{위치 임베딩}(t)\]

구현은 Ch10의 룩업 테이블 하나 더 — nn.Embedding(최대길이, d) 이다. 파라미터 수는 \(L_{\max} \times d\).

\[ \underbrace{\begin{pmatrix} 0.3 \\ -1.2 \\ 0.9 \\ 0.1 \end{pmatrix}}_{\text{"생선을"의 의미}} \;+\; \underbrace{\begin{pmatrix} 0.0 \\ 0.4 \\ -0.2 \\ 0.5 \end{pmatrix}}_{\text{"2번째 자리"}} \;=\; \underbrace{\begin{pmatrix} 0.3 \\ -0.8 \\ 0.7 \\ 0.6 \end{pmatrix}}_{\text{"2번째 자리의 생선을"}} \]

왜 이어 붙이지(concat) 않고 더하는가? 이어 붙이면 차원이 늘어나 뒤의 모든 층이 커진다. 더하면 차원이 그대로다. “정보가 섞여 버리지 않나?” 싶지만 — \(d\)가 수백 차원이면 학습이 알아서 의미 성분과 위치 성분을 다른 방향에 나누어 담는다. 실제로 학습된 위치 임베딩을 보면 서로 다른 위치끼리 뚜렷이 구별되는 패턴이 나타난다.

노트

원 논문은 학습이 필요 없는 sin/cos 공식으로 위치 벡터를 만들었다. 지금은 학습형 위치 임베딩(GPT 계열)이나 회전형(RoPE) 방식이 더 흔하다. 이 수업에서는 학습형만 다룬다 — 구현이 nn.Embedding 한 줄이고, 원리는 “위치마다 다른 벡터를 더한다”로 같다.


2. 깊이 — 잔차 연결과 LayerNorm

블록을 12개, 96개 쌓으려 한다. Ch04에서 배운 문제가 바로 앞을 막는다 — 층이 깊어지면 그래디언트가 앞쪽까지 도달하지 못한다.

잔차 연결 — Ch07에서 이미 만난 해법

잔차 연결 (Residual Connection)

어떤 층 \(F\) 를 통과한 결과에 입력을 그대로 더한다.

\[\text{출력} = F(x) + x\]

Ch07의 ResNet에서 본 그 구조 그대로다. 우회로를 따라 그래디언트가 곱셈 없이 통과하므로 깊은 층까지 살아서 도달한다. Transformer는 이것을 블록마다 두 번 쓴다 — 어텐션 뒤에 한 번, FFN 뒤에 한 번.

덧셈이 되려면 모양이 같아야 한다. Ch11에서 Multi-Head의 입출력 모양이 \((T, d)\) 로 보존된다고 강조한 이유가 여기서 드러난다.

LayerNorm — 이 장의 유일한 새 계산

정의: 층 정규화 (Layer Normalization)

토큰 하나의 \(d\)개 성분을 평균 0, 표준편차 1로 맞춘 뒤, 학습되는 \(\gamma, \beta\)로 다시 조정한다.

\[\hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}}, \qquad y_i = \gamma_i \hat{x}_i + \beta_i\]

\(\mu, \sigma\) 는 그 토큰의 \(d\)개 값으로만 계산한다 (다른 토큰과 무관).

Ch04의 표준화 \((x-\mu)/s\) 와 완전히 같은 식이다. 다른 것은 두 가지뿐이다 — 어느 방향으로 통계를 내는가, 그리고 뒤에 \(\gamma, \beta\) 가 붙는가.

✍️ 손으로 따라 계산하기 — LayerNorm

어떤 토큰의 벡터가 \(x = (2,\ 4,\ 4,\ 6)\) 이다. \(\gamma = 1, \beta = 0\) 이라 하자.

단계 1 — 평균.

\[\mu = \frac{2+4+4+6}{4} = 4\]

단계 2 — 분산과 표준편차 (Ch04와 같이 분모는 \(n\)).

\[\sigma^2 = \frac{(-2)^2 + 0^2 + 0^2 + 2^2}{4} = \frac{8}{4} = 2, \qquad \sigma = \sqrt{2} = 1.4142\]

단계 3 — 정규화.

\[\hat{x} = \left(\frac{-2}{1.4142},\ 0,\ 0,\ \frac{2}{1.4142}\right) = (-1.4142,\ 0,\ 0,\ 1.4142)\]

단계 4 — 검산. 평균이 0, 표준편차가 1인가?

\[\text{평균} = 0, \qquad \sigma^2 = \frac{2+0+0+2}{4} = 1 \quad ✅\]

코드
x = np.array([2., 4., 4., 6.])
mu, var = x.mean(), x.var()          # var는 분모 n
xhat = (x - mu) / np.sqrt(var + 1e-5)

pd.DataFrame({
    '원본 x': x,
    'x - μ': x - mu,
    '(x-μ)²': (x - mu) ** 2,
    '정규화 x̂': xhat.round(4),
}).to_html(index=False, border=0)
표 2: LayerNorm 손계산 검산
원본 x x - μ (x-μ)² 정규화 x̂
2.0 -2.0 4.0 -1.4142
4.0 0.0 0.0 0.0000
4.0 0.0 0.0 0.0000
6.0 2.0 4.0 1.4142
BatchNorm이 아니라 LayerNorm인 이유
어느 방향으로 통계를 내나 문제
BatchNorm 배치 안의 여러 데이터를 가로질러 문장 길이가 제각각이고 패딩이 섞여 통계가 불안정
LayerNorm 한 토큰의 \(d\)개 성분끼리 다른 토큰·다른 문장과 무관 → 안정적

텍스트는 길이가 제각각(Ch10의 패딩)이라 배치 방향 통계를 믿을 수 없다. 그래서 Transformer는 토큰 하나 안에서 정규화한다.

\(\gamma, \beta\) 는 각각 \(d\)개씩 — LayerNorm 하나의 파라미터는 \(2d\) 다.


3. 가공 — FFN (Feed-Forward Network)

어텐션이 하는 일을 다시 보자. 가중평균 — 즉 다른 토큰의 Value를 섞을 뿐, 각 위치에서 값을 가공하지는 않는다. 비선형 변환이 필요하다.

정의: FFN (Position-wise Feed-Forward Network)

토큰마다 독립적으로 적용되는 2층 완전연결 신경망. 차원을 넓혔다가 다시 줄인다.

\[\text{FFN}(x) = W_2 \,\text{ReLU}(W_1 x + b_1) + b_2, \qquad d \to 4d \to d\]

Ch02의 다층 퍼셉트론 그대로다. 새로운 것은 없다.

  • 왜 4배로 넓히나 — 좁은 공간에서 곧바로 비선형 변환을 하면 표현력이 부족하다. 넓은 공간으로 펼쳐 ReLU로 가공한 뒤 다시 접는다. 4배는 원 논문의 관례이고 하이퍼파라미터다
  • “position-wise” — 토큰 100개면 같은 FFN을 100번 적용한다. Ch06의 가중치 공유와 같은 발상이다

\[\text{FFN 파라미터} = \underbrace{d \times 4d + 4d}_{1\text{층}} + \underbrace{4d \times d + d}_{2\text{층}} = 8d^2 + 5d\]


4. 블록 조립

세 부품이 모였다. 이제 순서대로 쌓으면 Transformer 블록이다.

입력 (T, d)

Multi-Head Self-Attention

+ 잔차

LayerNorm

FFN d → 4d → d

+ 잔차

LayerNorm 출력 (T, d)

블록 1개 이것을 N번 쌓는다

입력도 \((T, d)\), 출력도 \((T, d)\) — 모양이 보존되므로 블록을 그대로 이어 붙일 수 있다.

✍️ 손으로 따라 계산하기 — 블록 하나의 파라미터 수

\(d = 768\) 인 블록 하나의 파라미터를 세어 보자.

부품 공식 \(d=768\)
Multi-Head Attention \(4d^2 + 4d\) 2,362,368
FFN \(8d^2 + 5d\) 4,722,432
LayerNorm × 2 \(4d\) 3,072
합계 \(\mathbf{12d^2 + 13d}\) 7,087,872

\[\boxed{\text{블록 하나} \approx 12 d^2}\]

코드
d = 768
rows = [
    ('Multi-Head Attention', '4d² + 4d', 4*d*d + 4*d),
    ('FFN (d→4d→d)',         '8d² + 5d', 8*d*d + 5*d),
    ('LayerNorm × 2',        '4d',       4*d),
]
tot = sum(r[2] for r in rows)
rows.append(('합계', '12d² + 13d', tot))
pd.DataFrame(rows, columns=['부품', '공식', f'd={d}']).to_html(index=False, border=0)
표 3: 블록 하나의 파라미터 수 (d = 768)
부품 공식 d=768
Multi-Head Attention 4d² + 4d 2362368
FFN (d→4d→d) 8d² + 5d 4722432
LayerNorm × 2 4d 3072
합계 12d² + 13d 7087872

5. 블록을 쌓으면 Transformer

블록을 \(N\)개 쌓고, 앞뒤에 임베딩과 출력층을 붙이면 완성이다.

\[\text{토큰 임베딩} + \text{위치 임베딩} \;\to\; \underbrace{[\text{블록}] \times N}_{\text{본체}} \;\to\; \text{출력층}\]

✍️ 실제 모델의 크기를 우리 공식으로 맞춰보기

GPT-2 small의 설정은 공개되어 있다: \(d = 768\), 블록 12층, 어휘 \(V = 50{,}257\), 최대 길이 1,024.

코드
d, n_layer, V, L = 768, 12, 50257, 1024

blocks = n_layer * (12*d*d + 13*d)
tok_emb = V * d
pos_emb = L * d
total = blocks + tok_emb + pos_emb

pd.DataFrame([
    ('블록 12층',      f'12 × (12·{d}² + 13·{d})', f'{blocks:,}'),
    ('토큰 임베딩',    f'{V:,} × {d}',             f'{tok_emb:,}'),
    ('위치 임베딩',    f'{L:,} × {d}',             f'{pos_emb:,}'),
    ('합계',           '',                          f'{total:,}'),
], columns=['부품', '계산', '파라미터 수']).to_html(index=False, border=0)
표 4: GPT-2 small의 파라미터 수를 공식으로 계산
부품 계산 파라미터 수
블록 12층 12 × (12·768² + 13·768) 85,054,464
토큰 임베딩 50,257 × 768 38,597,376
위치 임베딩 1,024 × 768 786,432
합계 124,438,272

약 1억 2,400만. 공개된 GPT-2 small의 파라미터 수가 124M이다.

여러분은 방금 실제 LLM의 크기를 손으로 맞췄다

블랙박스가 아니다. \(d\)와 층 수만 알면 크기를 계산할 수 있는 구조물이다. 이 구조가 커진 것이 지금의 모든 LLM이다.

모델 \(d\) 층 수 파라미터
GPT-2 small 768 12 124M
GPT-2 large 1,280 36 774M
GPT-3 12,288 96 175B

\(12d^2 N\) 이라는 식 하나로 세 줄이 전부 설명된다 — \(d\)를 키우면 제곱으로, 층을 늘리면 비례해서 커진다.


6. 이 블록으로 무엇을 하는가 — 언어모델

구조는 완성됐다. 이제 출력층에 무엇을 붙이고, 무엇을 목표로 학습시킬 것인가를 정할 차례다.

LLM의 목표는 단 하나: 다음 토큰 예측

빈칸 하나 맞히기

다음 문장의 빈칸에 무엇이 올지 맞혀 보자.

“나는 어제 학교에 ___”

“갔다”가 가장 그럴듯하고, “왔다”도 가능하다. “고양이”는 문법적으로 이상하다. 여러분의 머릿속에서 방금 일어난 일 — 문맥을 보고 다음 단어의 그럴듯함에 점수를 매기는 것 — 이것이 ChatGPT 같은 거대 언어모델(LLM)이 하는 일의 전부다. 수천억 개의 파라미터, 수조 개의 학습 토큰, 그 모든 규모가 향하는 목표는 단 하나, 이 빈칸 맞히기다.

정의: 언어모델 (Language Model)

문맥(지금까지의 토큰들)이 주어졌을 때, 다음 토큰의 확률분포를 출력하는 모델.

\[P(\text{다음 토큰} \mid \text{문맥})\]

이 확률분포에서 토큰을 하나 뽑아 문맥에 붙이고, 다시 다음 토큰을 예측하는 일을 반복하면 문장이 생성된다. LLM(Large Language Model)은 이것을 아주 큰 규모로 하는 언어모델이다.

이것은 여러분이 아는 문제다

“다음 토큰 예측”이라는 말을 이 수업의 언어로 번역하면 이렇게 된다:

클래스가 어휘 전체인 다중 분류 문제.

Ch03에서 3개 클래스 분류를 다뤘다. 출력층에 노드 3개를 두고, Softmax로 확률로 바꾸고, 교차 엔트로피로 손실을 계산했다. LLM은 클래스 수가 3개가 아니라 어휘 크기(수만 개) 일 뿐, 구조가 똑같다:

Ch03의 3-클래스 분류 LLM의 다음 토큰 예측
입력 특성 벡터 문맥 (토큰열)
출력층 노드 수 3개 어휘 크기 (예: GPT-3는 50,257개)
확률 변환 Softmax (Ch03) Softmax (같은 함수)
손실 함수 교차 엔트로피 (Ch03) 교차 엔트로피 (같은 함수)
정답 클래스 라벨 실제로 다음에 온 토큰

정답 토큰의 확률을 \(\hat{y}_{\text{정답}}\)이라 하면 손실은 Ch03 그대로 \(\mathcal{L} = -\log(\hat{y}_{\text{정답}})\)이다.

여러분은 이미 LLM의 손실 함수를 3주차에 손으로 계산했다. 문맥을 벡터로 바꾸는 내부(임베딩은 Ch10, 어텐션은 Ch11)도 이미 배웠으니, LLM에서 여러분이 계산할 수 없는 부분은 사실상 없다 — 크기가 클 뿐이다.


다음 토큰 확률 손계산

어휘가 딱 5개인 장난감 언어모델을 만들자. 어휘: 갔다, 왔다, 있다, 먹었다, 고양이.

문맥 “나는 어제 학교에”를 입력하면, 모델의 출력층(어휘 크기만큼의 노드)이 로짓 5개를 내놓는다:

\[z = (2.0,\ 1.0,\ 0.5,\ -0.5,\ -1.5)\]

Ch03의 Softmax를 그대로 적용하면 다음 토큰의 확률분포가 나온다.

코드
import pandas as pd
import numpy as np

tokens = ['갔다', '왔다', '있다', '먹었다', '고양이']
z = [2.0, 1.0, 0.5, -0.5, -1.5]

exp_z = [np.exp(zi) for zi in z]
sum_exp = sum(exp_z)
probs = [e / sum_exp for e in exp_z]

rows = []
for t, zi, e, p in zip(tokens, z, exp_z, probs):
    rows.append({
        '토큰': t,
        '로짓 $z_k$': zi,
        '$e^{z_k}$': f'{e:.4f}',
        '확률 $\\hat{y}_k$': f'{p:.4f}',
        '확률 (%)': f'{p*100:.1f}%',
    })
rows.append({
    '토큰': '**합계**',
    '로짓 $z_k$': '',
    '$e^{z_k}$': f'{sum_exp:.4f}',
    '확률 $\\hat{y}_k$': f'{sum(probs):.4f}',
    '확률 (%)': '100.0%',
})

pd.DataFrame(rows).to_html(index=False, border=0, escape=False)
표 5: 다음 토큰 확률 손계산 — 문맥: ‘나는 어제 학교에’
토큰 로짓 \(z_k\) \(e^{z_k}\) 확률 \(\\hat{y}_k\) 확률 (%)
갔다 2.0 7.3891 0.5871 58.7%
왔다 1.0 2.7183 0.2160 21.6%
있다 0.5 1.6487 0.1310 13.1%
먹었다 -0.5 0.6065 0.0482 4.8%
고양이 -1.5 0.2231 0.0177 1.8%
합계 12.5857 1.0000 100.0%

“갔다”가 58.7%로 가장 그럴듯하고, “고양이”는 1.8%다. 학습 데이터에서 실제 다음 토큰이 “갔다”였다면, 손실은 Ch03 그대로 \(\mathcal{L} = -\log(0.5871) = 0.5326\). 실제 LLM의 학습은 이 계산을 수조 개의 빈칸에 대해 반복하며 경사하강법(Ch03~04)으로 손실을 줄이는 것이다.


Temperature: 확률분포의 뾰족함을 조절하는 손잡이

정의

ChatGPT API의 설정 화면에서 보게 될 temperature는 새로운 수학이 아니라, 방금 쓴 Softmax에 나누기 하나를 추가한 것이다.

정의: Temperature가 있는 Softmax

로짓을 온도 \(T > 0\)로 나눈 뒤 Softmax를 적용한다.

\[\text{softmax}_T(z_k) = \frac{e^{z_k / T}}{\sum_{j=1}^{K} e^{z_j / T}}\]

  • \(T = 1\): 보통의 Softmax
  • \(T < 1\): 로짓 차이가 증폭 → 분포가 뾰족해짐 (1등에 확률 집중)
  • \(T > 1\): 로짓 차이가 축소 → 분포가 평평해짐 (확률이 고르게 퍼짐)

손계산

같은 로짓 \(z = (2.0, 1.0, 0.5, -0.5, -1.5)\)에 \(T = 0.5,\ 1,\ 2\)를 적용해 한 표에서 비교하자. 예를 들어 \(T = 0.5\)면 로짓이 \((4.0, 2.0, 1.0, -1.0, -3.0)\)으로 벌어지고, \(T = 2\)면 \((1.0, 0.5, 0.25, -0.25, -0.75)\)로 좁혀진다.

코드
import pandas as pd
import numpy as np

tokens = ['갔다', '왔다', '있다', '먹었다', '고양이']
z = np.array([2.0, 1.0, 0.5, -0.5, -1.5])

def softmax_T(z, T):
    e = np.exp(z / T)
    return e / e.sum()

p_half = softmax_T(z, 0.5)
p_one  = softmax_T(z, 1.0)
p_two  = softmax_T(z, 2.0)

rows = []
for i, t in enumerate(tokens):
    rows.append({
        '토큰': t,
        '로짓 $z_k$': z[i],
        '$T=0.5$': f'{p_half[i]*100:.1f}%',
        '$T=1$': f'{p_one[i]*100:.1f}%',
        '$T=2$': f'{p_two[i]*100:.1f}%',
    })

pd.DataFrame(rows).to_html(index=False, border=0, escape=False)
표 6: 같은 로짓, 다른 temperature — softmax(z/T) 비교
토큰 로짓 \(z_k\) \(T=0.5\) \(T=1\) \(T=2\)
갔다 2.0 83.8% 58.7% 39.4%
왔다 1.0 11.3% 21.6% 23.9%
있다 0.5 4.2% 13.1% 18.6%
먹었다 -0.5 0.6% 4.8% 11.3%
고양이 -1.5 0.1% 1.8% 6.8%

로짓은 하나도 바뀌지 않았는데, “갔다”의 확률이 \(T=0.5\)에서 83.8%, \(T=2\)에서 39.4%로 달라진다. Temperature는 모델의 판단을 바꾸는 것이 아니라 판단의 확신 정도를 표현하는 방식을 바꾼다.

해석: 언제 낮추고 언제 높이는가

  • \(T\)를 낮추면 분포가 뾰족해져 거의 항상 1등 토큰이 뽑힌다 — 출력이 결정적이고 일관된다. 사실 확인, 요약, 분류처럼 “정답이 하나에 가까운” 작업에 적합하다.
  • \(T\)를 높이면 분포가 평평해져 2, 3등 토큰도 자주 뽑힌다 — 출력이 다양해진다. 브레인스토밍, 초안 작성, 창작처럼 “여러 답이 가능한” 작업에 적합하다.

확률분포에서 실제 토큰을 고르는 방식도 두 갈래다. 그리디(greedy) 디코딩은 매번 확률 1등 토큰만 택한다 — 같은 입력에 항상 같은 출력이 나오지만 뻔하고 반복적인 문장이 되기 쉽다. 샘플링은 확률분포대로 주사위를 던져 뽑는다 — “갔다”가 58.7%면 100번 중 59번쯤 “갔다”가 나오고 나머지는 다른 토큰이 나온다. ChatGPT가 같은 질문에 매번 조금씩 다른 답을 내놓는 이유가 바로 이 샘플링이고, temperature는 그 주사위의 모양을 조절하는 손잡이다.


인터랙티브 — Temperature 손잡이를 직접 돌려 보세요

로짓 5개는 위 손계산과 같은 값으로 고정되어 있어요. T 슬라이더를 움직이면 Softmax 확률 막대가 실시간으로 변합니다. T를 0.1까지 내리면 어떤 일이 생기는지, 3까지 올리면 균등분포(점선, 20%)에 얼마나 가까워지는지 확인해 보세요.

시도해 보세요: T = 0.1에서 “갔다”가 사실상 100%가 되는 것 — 이것이 그리디 디코딩과 같아지는 극한입니다. 반대로 T = 3에서도 다섯 막대가 완전히 같아지지는 않는 이유(로짓 순서는 보존됨)를 표의 \(z\) 값으로 설명해 보세요.



깊이 계약 확인

문제집과 시험이 다루는 것은 네 가지다.

  1. LayerNorm 손계산 — 평균·분산(분모 \(n\))·정규화
  2. 블록 파라미터 수 — \(12d^2 + 13d\), 그리고 모델 전체 크기
  3. 다음 토큰 확률 — 로짓에 Softmax
  4. Temperature — \(\text{softmax}(z/T)\) 계산과 해석

범위 밖: 위치 인코딩의 sin/cos 수식, Pre-LN과 Post-LN의 차이, 어텐션·LayerNorm의 역전파 유도, 실제 사전학습 절차.


핵심 요약

개념 핵심 내용
순서의 소실 어텐션은 토큰을 섞어도 결과가 같다 — 위치 정보가 들어갈 자리가 없다
위치 임베딩 위치마다 벡터를 만들어 토큰 임베딩에 더한다. 파라미터 \(L_{\max} \times d\)
잔차 연결 \(F(x) + x\) — Ch07 ResNet 그대로. 블록마다 두 번
LayerNorm 토큰 하나의 \(d\)개 성분을 평균 0·표준편차 1로. 파라미터 \(2d\)
BatchNorm이 아닌 이유 문장 길이가 제각각이라 배치 방향 통계가 불안정
FFN \(d \to 4d \to d\) 의 2층 MLP, 토큰마다 독립 적용. 파라미터 \(8d^2 + 5d\)
블록 MHA → 잔차 → LN → FFN → 잔차 → LN. 입출력 모양 \((T,d)\) 보존
블록 파라미터 \(12d^2 + 13d\) — 실제 모델 크기를 이 공식으로 맞출 수 있다
언어모델 다음 토큰 예측 = 클래스가 어휘 전체인 다중 분류 (Ch03 그대로)
Temperature \(\text{softmax}(z/T)\) — \(T\)↓ 뾰족(결정적), \(T\)↑ 평평(다양)

다음 장 예고: 구조는 다 알았다. 그런데 이것을 인터넷 규모의 텍스트로, 수천억 파라미터로 학습시키면 무슨 일이 생기는가? 그리고 남이 학습시켜 놓은 그 모델을 내가 어떻게 가져다 쓰는가? — 사전학습 생태계와 활용이 마지막 장이다.