SONUB
KO English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

알림을 켜는 방법

이 브라우저 또는 주소에서는 알림을 사용할 수 없습니다. 지원되는 브라우저로 보안 연결된 SONUB 사이트에 접속해 주세요.

알림이 차단되어 있으면 브라우저 설정에서 허용해 주세요.

  1. 주소 표시줄 옆의 사이트 설정 또는 브라우저의 사이트 설정을 열고, 이 사이트의 알림을 허용으로 바꾸세요.
  2. 기기 설정에서 브라우저 알림을 허용하고 방해 금지 또는 집중 모드를 확인하세요.
  3. SONUB으로 돌아와 페이지를 새로고침하세요. 알림이 켜지면 벨은 사라집니다.

iPhone 또는 iPad에서는 SONUB을 홈 화면에 추가하고 거기에서 연 뒤 알림을 허용하세요.

로그인 로그아웃 화면 테마 기본 테마 다크 테마 핑크 테마 채팅 1:1 채팅 그룹 채팅 게시판 공지사항 뉴스 게시판 건의 게시판 자기소개 게시판 메인 메뉴

자, 이제 여러분들만의 인공지능을 만들어 봅시다.

아주 작은 와 를 직접 작성합니다. engine.py 와 model.py 로 ·인과 · 를 만들고 출력 모양을 확인해 보세요.

단어 블록이 한 줄로 놓여 있고 끝에 빈칸이 있다. 빈칸 위에 높이가 다른 막대를 단 후보 블록 세 개가 있고, 그중 하나가 골라져 줄 끝에 붙은 뒤 화살표가 다시 처음으로 돌아가 다음 칸을 고른다.
5

GPT 구조와 코드

은 로 숫자 를 찾는 표입니다. 를 더하면 몇 번째 조각인지 알 수 있습니다. 는 찾을 정보, Key는 각 앞자리의 특징, Value는 가져올 정보입니다. 은 관련도를 로 바꾸어 Value를 섞습니다.

은 인과적 어텐션 하나와 하나입니다. keys와 values에는 현재와 앞자리만 들어가므로 미래 정답을 훔쳐볼 수 없습니다. 은 원래 정보를 남기고, 두 층 는 특징을 바꿉니다. 은 숫자 크기를 안정시키고 은 각 의 점수를 냅니다. 읽기 쉽게 , 변수가 없는 정규화, 없는 구조로 줄였습니다.

전체 구조 — 토큰 번호에서 다음 토큰 확률까지

의 는 입력 토큰마다 아래 계산을 한 번씩 합니다. 구조는 와 같은 전용 이고, 블록이 1개·헤드가 1개일 뿐입니다.

  • 토큰 번호로 임베딩 표에서 벡터(8칸)를 꺼내고, 자리 번호로 위치 벡터를 꺼내 더합니다.
  • 정규화한 뒤 인과 어텐션으로 앞자리들의 정보를 섞고, 원래 벡터에 더합니다(잔차).
  • 다시 정규화한 뒤 MLP 로 가공하고, 또 원래 벡터에 더합니다(잔차).
  • 마지막으로 정규화하고 출력층(head)으로 40개마다 점수()를 냅니다.
  • 점수를 softmax 로 확률로 바꾸면 다음 토큰의 확률입니다.

실제 GPT 는 가운데 블록(어텐션 + MLP)을 여러 번 쌓습니다. GPT-2 Small 은 12번, GPT-3 는 96번입니다. 블록마다 앞 블록이 만든 벡터를 받아 조금씩 더 다듬습니다. 블록의 모양이 같아서 몇 번이든 쌓을 수 있습니다.

GPT 한 번의 계산 — 토큰 번호에서 다음 토큰 확률까지
  1. 토큰 번호
  2. 임베딩 + 위치
  3. 인과 어텐션
  4. MLP
  5. 출력층 점수
  6. 다음 토큰 확률

임베딩 — 번호로 표의 한 줄을 꺼낸다

임베딩(embedding)은 어휘 수 × 폭 크기의 표입니다. XSLM 은 40줄 × 8칸입니다. 토큰 번호가 5면 다섯째 줄(0부터 셈)의 숫자 8개를 꺼낼 뿐입니다. 곱셈도 검색도 없는 표 찾기(lookup)입니다.

왜 번호를 그대로 쓰지 않고 벡터로 바꿀까요? 번호에는 크기와 순서만 있어서 "9번과 23번이 비슷하다" 같은 관계를 담을 수 없습니다. 숫자 8개로 된 벡터는 여러 방향으로 서로 가깝거나 멀 수 있어, 학습이 비슷하게 쓰이는 토큰의 벡터를 서로 가깝게 옮겨 놓을 수 있습니다.

임베딩 표도 모델의 입니다. 처음에는 무작위 숫자이고, 학습이 을 줄이는 방향으로 고칩니다. 따로 만든 을 가져오지 않습니다. 아래 실험으로 무작위로 시작한 5번 토큰의 벡터와 위치 0의 벡터를 직접 보고, 9개 의 크기를 세어 보세요.

직접 확인하기 lab/embedding.py
from model import GPT

m = GPT(40, 8, 12)
print('token 5   :', [round(v.data, 3) for v in m.weights['token'][5]])
print('position 0:', [round(v.data, 3) for v in m.weights['position'][0]])
for name, matrix in m.weights.items():
    rows, cols = len(matrix), len(matrix[0])
    print(name, rows, 'x', cols, '=', rows * cols)
print('total', len(m.parameters))
cd ~/xslm
mkdir -p lab
micro lab/embedding.py
python3 -m lab.embedding

실행 결과

token 5   : [-0.081, 0.024, 0.043, -0.149, 0.005, 0.131, -0.201, -0.032]
position 0: [-0.102, -0.036, 0.089, 0.01, -0.085, -0.084, 0.267, 0.114]
token 40 x 8 = 320
position 12 x 8 = 96
query 8 x 8 = 64
key 8 x 8 = 64
value 8 x 8 = 64
project 8 x 8 = 64
expand 16 x 8 = 128
contract 8 x 16 = 128
head 40 x 8 = 320
total 1248

임베딩 공간 — 가까우면 비슷하다

임베딩은 물건이나 낱말을 숫자 몇 개로 적은 것입니다. 숫자 하나가 축 하나입니다. 아래 지도는 숫자 2개, 빠르기(가로축)와 크기(세로축)로 물건을 적었습니다. 0 은 가장 느리고 작고, 1 은 가장 빠르고 큽니다. 소는 [0.1, 0.8] 이라 느리고 큽니다. 독수리는 [0.9, 0.4] 라 빠르고 작습니다.

숫자로 적으면 "얼마나 비슷한가"를 거리로 잴 수 있습니다. 두 점 [x₁, y₁] 과 [x₂, y₂] 사이의 는 √((x₁ − x₂)² + (y₁ − y₂)²) 입니다. 거리가 짧을수록 비슷합니다. 알람시계 [0, 0] 에서 재면 이렇습니다.

  • 자전거 [0.3, 0.3]: √(0.09 + 0.09) = 0.424
  • 소 [0.1, 0.8]: √(0.01 + 0.64) = 0.806
  • 독수리 [0.9, 0.4]: √(0.81 + 0.16) = 0.985

그래서 알람시계와 가장 닮은 것은 자전거입니다. 지도에서 점을 누르면 그 점이 기준이 되고, 빈 곳을 누르면 그 자리가 기준이 됩니다. 가까운 순서가 바로 다시 정해집니다.

의 임베딩도 같은 생각입니다. 다른 점은 둘입니다. 축이 2개가 아니라 수백에서 수천 개이고(XSLM 은 8개), 축의 뜻을 사람이 정하지 않고 학습이 정합니다. 그래서 축마다 "빠르기" 같은 이름은 없지만, 비슷하게 쓰이는 토큰의 벡터가 서로 가까이 모입니다.

거리 재기는 인공지능 서비스 곳곳에 쓰입니다. 는 질문의 임베딩과 가장 가까운 문서를 찾고, 는 그렇게 찾은 문서를 에게 함께 건넵니다. MariaDB 같은 도 VECTOR 칸과 VEC_DISTANCE_EUCLIDEAN 함수로 같은 계산을 합니다. 아래 실험은 같은 지도를 으로 계산합니다.

빠르기와 크기, 숫자 2개로 적은 임베딩 지도입니다. 점이나 빈 곳을 누르면 그 자리에서 가까운 순서를 다시 잽니다.

크기 (Size)

1 큼0.50 작음

0 느림0.51 빠름

빠르기 (Speed)

가까운 순서

기준점 알람시계 [0, 0]

  1. 1 알람시계 0.000
  2. 2 자전거 0.424
  3. 3 나 0.680
  4. 4 소 0.806
  5. 5 코끼리 0.906
  6. 6 배 0.949
  7. 7 독수리 0.985
  8. 8 말 0.990
  9. 9 자동차 1.238
  10. 10 로켓 1.372

거리 = √((x₁ − x₂)² + (y₁ − y₂)²)

직접 확인하기 lab/distance.py
import math

# A 2-number embedding: [speed, size]. 0 is the slowest and smallest, 1 the fastest and biggest.
things = {
    'alarm clock': [0.0, 0.0],
    'cow': [0.1, 0.8],
    'bicycle': [0.3, 0.3],
    'eagle': [0.9, 0.4],
    'elephant': [0.1, 0.9],
    'me': [0.2, 0.65],
    'horse': [0.7, 0.7],
    'car': [0.9, 0.85],
    'ship': [0.3, 0.9],
    'rocket': [0.99, 0.95],
}


def distance(a, b):
    return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)


for query in [[0.0, 0.0], [0.1, 0.8], [0.5, 0.5]]:
    print('from', query)
    nearest = sorted(things, key=lambda name: distance(things[name], query))
    for name in nearest[:4]:
        print(f'  {name:12} {distance(things[name], query):.3f}')
cd ~/xslm
mkdir -p lab
micro lab/distance.py
python3 -m lab.distance

실행 결과

from [0.0, 0.0]
  alarm clock  0.000
  bicycle      0.424
  me           0.680
  cow          0.806
from [0.1, 0.8]
  cow          0.000
  elephant     0.100
  me           0.180
  ship         0.224
from [0.5, 0.5]
  horse        0.283
  bicycle      0.283
  me           0.335
  eagle        0.412

위치 임베딩 — 몇 번째 자리인지 알려 준다

어텐션은 앞자리들을 섞을 때 순서를 따지지 않습니다. 위치 정보가 없으면 "dog can run" 과 "run can dog" 의 토큰 벡터가 같은 묶음이 됩니다. 그래서 자리마다 학습하는 위치 벡터를 하나씩 두고 토큰 벡터에 더합니다.

XSLM 의 위치 표는 (12)줄 × 8칸입니다. 줄이 12개뿐이므로 한 번에 넣을 수 있는 토큰도 12개까지입니다. 의 첫 줄 가 이것을 검사하고, generate 는 그래서 ids[-12:] 처럼 마지막 12토큰만 넣습니다. 이 한계를 문맥 길이()라고 합니다. GPT-2 는 1,024토큰이고, 최근 모델은 수십만 토큰입니다.

GPT-2 와 XSLM 은 위치마다 벡터를 학습합니다. 처음 Transformer 논문은 sin·cos 로 위치 벡터를 계산했고, 최근 모델은 Query·Key 를 위치만큼 돌리는 를 많이 씁니다. 방법은 달라도 목적은 순서를 알게 하는 것입니다.

선형층·정규화·ReLU — 모델을 이루는 세 가지 계산

engine.py 의 세 개가 모델 계산의 대부분입니다.

  • linear(x, W): 행렬 W 의 줄마다 x 와 (같은 칸끼리 곱해 모두 더함)을 합니다. W 가 3줄이면 결과도 3칸입니다. 실험에서 x = [1, 2] 와 줄 [0.5, 0.5] 의 내적은 0.5 + 1.0 = 1.5 입니다. 줄 수를 바꾸면 벡터의 칸 수를 늘리거나 줄일 수 있습니다.
  • norm(x): 칸들의 평균을 빼고 표준편차로 나눠 평균 0·분산 1로 맞춥니다. [2, 4, 6] 은 [-1.225, 0, 1.225] 가 됩니다. 계산이 거듭되어도 숫자가 너무 커지거나 작아지지 않게 합니다. 0으로 나누지 않으려고 분산에 1e-5 를 더합니다.
  • relu(v): 음수는 0, 양수는 그대로 둡니다.

ReLU 같은 비선형 함수가 꼭 필요합니다. 선형층 두 개를 이어 붙여도 결국 선형층 하나와 같아서, 아무리 쌓아도 직선 같은 관계밖에 표현하지 못합니다. 사이에 꺾이는 함수를 넣어야 복잡한 관계를 배웁니다.

XSLM 의 linear 에는 더하는 값(bias)이 없고, norm 에는 학습하는 크기·이동 값(γ·β)이 없습니다. 의 nn.Linear·nn.LayerNorm 에는 둘 다 있습니다.

직접 확인하기 lab/linear.py
from engine import linear, norm

x = [1.0, 2.0]
W = [[1.0, 0.0], [0.5, 0.5], [-1.0, 1.0]]
print('linear:', linear(x, W))
print('norm  :', [round(v, 3) for v in norm([2.0, 4.0, 6.0])])
print('relu  :', [max(0.0, v) for v in [-2.0, 0.5, 3.0]])
cd ~/xslm
mkdir -p lab
micro lab/linear.py
python3 -m lab.linear

실행 결과

linear: [1.0, 1.5, 1.0]
norm  : [-1.225, 0.0, 1.225]
relu  : [0.0, 0.5, 3.0]

어텐션 — Query·Key·Value 로 앞자리를 섞는다

어텐션은 지금 자리가 앞자리들 가운데 무엇을 얼마나 참고할지 정해 그 정보를 섞습니다. 자리마다 벡터 세 개를 만듭니다.

  • Query(q): 지금 자리가 찾는 것.
  • Key(k): 각 자리가 "나에게는 이런 정보가 있다"고 내보이는 특징.
  • Value(v): 각 자리가 실제로 건네줄 내용.

계산은 네 걸음입니다.

  • 점수: 지금 자리의 q 와 각 자리 k 의 내적을 √폭(XSLM 은 √8 ≈ 2.83)으로 나눕니다.
  • 비중: 점수를 softmax 로 합이 1인 비중으로 바꿉니다.
  • 섞기: 각 자리의 v 에 비중을 곱해 더합니다.
  • 정리: 섞은 결과를 project 행렬로 한 번 더 변환해 원래 벡터에 더합니다.

식으로 쓰면 softmax(q·kᵀ / √d)·v 입니다. 아래 실험은 손으로 만든 key 3개와 query 하나로 이 계산을 합니다(폭 2라서 √2 로 나눔). query [2, 0] 은 첫째 특징을 찾으므로 그 특징이 있는 1·3번째 자리가 0.446씩, 없는 2번째 자리가 0.108을 받습니다. 결과는 1·3번째 value 쪽으로 기운 [6.687, 3.313] 입니다.

예를 들어 "the cat sat because it was tired" 에서 it 자리의 어텐션은 cat 자리에 큰 비중을 주도록 학습될 수 있습니다. 실제 모델은 헤드를 여러 개(GPT-2 Small 은 12개) 두어, 헤드마다 다른 관계(가리키는 말·바로 앞 낱말·형식 표시 등)를 봅니다.

직접 확인하기 lab/attention.py
from engine import Value, softmax

keys = [[1, 0], [0, 1], [1, 1]]
values = [[10, 0], [0, 10], [5, 5]]
query = [2, 0]
scores = [sum(q * k for q, k in zip(query, key)) / 2 ** 0.5 for key in keys]
weights = softmax([Value(s) for s in scores])
mixed = [sum(w.data * v[i] for w, v in zip(weights, values)) for i in range(2)]
print('scores ', [round(s, 3) for s in scores])
print('weights', [round(w.data, 3) for w in weights])
print('mixed  ', [round(m, 3) for m in mixed])
cd ~/xslm
mkdir -p lab
micro lab/attention.py
python3 -m lab.attention

실행 결과

scores  [1.414, 0.0, 1.414]
weights [0.446, 0.108, 0.446]
mixed   [6.687, 3.313]

인과 마스크 — 미래 토큰을 보지 못하게

학습할 때는 구간 전체를 한 번에 넣고 자리마다 다음 토큰을 맞히게 합니다. 어떤 자리가 자기 뒤의 토큰을 볼 수 있다면 정답을 훔쳐보는 셈이라 아무것도 배우지 못합니다. 그래서 각 자리는 자기와 앞자리만 보게 합니다. 이것을 인과(causal) 어텐션이라고 합니다.

PyTorch 구현은 점수 표에서 대각선 위쪽(미래)을 −∞ 로 채워 softmax 뒤 0이 되게 합니다(하삼각 마스크). XSLM 은 더 단순합니다. 토큰을 앞에서부터 하나씩 처리하며 keys·values 목록에 지금까지의 것만 쌓으므로, 미래 토큰은 아직 목록에 없습니다.

그래서 토큰을 뒤에 더해도 앞자리의 결과는 바뀌지 않아야 합니다. 아래 실험은 [1, 2] 와 [1, 2, 3] 을 넣어 앞 두 자리의 점수가 똑같은지 확인합니다. True 가 나오면 미래를 보지 않는다는 증거입니다.

이 keys·values 목록은 실제 모델의 와 같은 생각입니다. 생성할 때 앞 토큰의 key·value 를 저장해 두면 새 토큰 하나만 계산하면 됩니다. XSLM 의 generate 는 단순하게 토큰마다 문맥 전체를 다시 계산합니다.

직접 확인하기 lab/causal.py
from model import GPT

m = GPT(10, 8, 12)
short = m.forward([1, 2])
long = m.forward([1, 2, 3])
same = [[v.data for v in r] for r in short] == [[v.data for v in r] for r in long[:2]]
print('first two positions unchanged:', same)
print('scores at position 3:', [round(v.data, 3) for v in long[2][:4]], '...')
cd ~/xslm
mkdir -p lab
micro lab/causal.py
python3 -m lab.causal

실행 결과

first two positions unchanged: True
scores at position 3: [0.159, 0.011, -0.239, 0.428] ...

softmax — 점수를 확률로, 그리고 수치 안정

softmax 는 점수 목록을 0~1 사이이고 합이 1인 확률로 바꿉니다. 각 점수에 exp 를 씌운 뒤 전체 합으로 나눕니다. exp 는 언제나 양수이고 큰 점수를 훨씬 크게 만들어, 순서는 지키면서 차이를 키웁니다. 실험에서 점수 [2, 1, 0.1] 은 확률 [0.659, 0.242, 0.099] 가 됩니다.

문제는 exp 가 아주 빨리 커진다는 것입니다. exp(1000) 은 컴퓨터의 실수가 담을 수 있는 범위를 넘어 OverflowError 가 납니다(실험의 math range error). 그래서 engine.py 의 softmax 는 먼저 가장 큰 점수(peak)를 모든 점수에서 뺍니다. 모든 점수에서 같은 수를 빼도 확률은 변하지 않으면서, 가장 큰 값이 exp(0) = 1 이 되어 넘치지 않습니다. [1000, 999] 도 [0.731, 0.269] 로 바르게 나옵니다.

softmax 는 어텐션 비중을 낼 때와 출력층 점수를 다음 토큰 확률로 바꿀 때 두 번 쓰입니다.

직접 확인하기 lab/softmax.py
import math
from engine import Value, softmax

probs = softmax([Value(s) for s in [2.0, 1.0, 0.1]])
print([round(p.data, 3) for p in probs], round(sum(p.data for p in probs), 3))
try:
    print([math.exp(s) for s in [1000.0, 999.0]])
except OverflowError as error:
    print('without max:', error)
print('with max:', [round(p.data, 3) for p in softmax([Value(1000.0), Value(999.0)])])
cd ~/xslm
mkdir -p lab
micro lab/softmax.py
python3 -m lab.softmax

실행 결과

[0.659, 0.242, 0.099] 1.0
without max: math range error
with max: [0.731, 0.269]

MLP — 자리마다 정보를 가공한다

어텐션이 "어디를 볼지" 정해 정보를 모았다면, MLP(다층 퍼셉트론, 이라고도 함)는 "모은 것을 어떻게 바꿀지"를 맡습니다. 자리마다 따로, 같은 가중치로 계산합니다.

  • expand: 8칸을 16칸으로 넓힙니다.
  • ReLU: 음수를 0으로 꺾습니다.
  • contract: 16칸을 다시 8칸으로 줄여 원래 벡터에 더합니다.

넓혔다 줄이는 까닭은 넓은 공간에서 여러 특징의 조합을 따로 켜고 끌 여유를 주기 위해서입니다. 큰 GPT 의 는 대부분 이 MLP 에 있고, 학습한 사실 같은 지식이 주로 이곳에 쌓인다고 여겨집니다.

XSLM 은 2배로 넓히고 ReLU 를 씁니다. GPT-2 는 4배로 넓히고 를 씁니다. GELU 는 0 근처에서 부드럽게 꺾이는 함수로 큰 모델에서 학습이 조금 더 잘 됩니다. XSLM 은 을 손으로 읽기 쉬운 ReLU 를 골랐습니다.

잔차 연결과 사전 정규화 — 깊게 쌓아도 학습되게

model.py 의 x = [a + b for a, b in zip(x, …)] 줄이 잔차 연결(residual connection)입니다. 층의 결과로 x 를 바꿔 치우지 않고 원래 x 에 더합니다.

  • 원래 정보가 그대로 남아, 층은 고칠 차이만 배우면 됩니다.
  • 때 덧셈은 를 그대로 전하므로 기울기가 지름길로 앞층까지 흘러갑니다. 블록을 수십 개 쌓아도 기울기가 사라지지 않습니다.

정규화의 자리도 중요합니다. XSLM 은 GPT-2 처럼 어텐션 앞·MLP 앞·출력층 앞에서 norm 을 합니다(Pre-LN). 층에 들어가는 값만 정규화하고 잔차 길은 그대로 두는 방식이라 깊은 모델도 안정적으로 학습됩니다. 처음 Transformer 는 더한 뒤에 정규화했습니다(Post-LN).

한 블록을 식으로 쓰면 이렇습니다.

  • x = x + Attention(norm(x))
  • x = x + MLP(norm(x))

출력층 — 어휘마다 점수(logit)를 낸다

블록을 지난 벡터를 마지막으로 정규화한 뒤 head 행렬(40줄 × 8칸)과 곱하면 어휘 40개마다 점수가 하나씩 나옵니다. 이 날것의 점수를 logit 이라고 합니다. logit 은 음수도 되고 합이 1도 아니며, softmax 를 거쳐야 확률이 됩니다.

forward 는 입력 자리마다 점수 목록을 하나씩 돌려줍니다. 토큰 2개를 넣으면 2 × 40 점수가 나옵니다. 학습은 모든 자리의 점수를 쓰고(자리마다 다음 토큰을 맞혀 봄), 생성은 마지막 자리의 점수 [-1] 만 씁니다.

GPT-2 는 출력층 행렬을 따로 두지 않고 토큰 임베딩 표를 함께 씁니다(weight tying). 파라미터가 줄고, 토큰을 읽을 때와 낼 때 같은 벡터를 쓰게 됩니다. XSLM 은 둘의 역할을 구별해 보이려고 따로 둡니다.

자동미분 — 계산 그래프를 거꾸로 따라간다

학습하려면 가중치 하나하나를 조금 바꿀 때 손실이 얼마나 바뀌는지(기울기, gradient)를 알아야 합니다. 1,248개를 하나씩 흔들어 보면 너무 느리므로, 미분의 으로 한 번에 구합니다. 이것을 역전파(backpropagation)라고 하고, 그 일을 자동으로 해 주는 장치가 (autograd)입니다. engine.py 의 Value 가 바로 그 장치입니다.

  • Value 는 계산할 때마다 결과와 함께 parents 에 (입력, 그 입력에 대한 국소 미분)을 적어 둡니다. 덧셈은 1, 곱셈은 상대편 값, exp 는 결과 자신, log 는 1/x, ReLU 는 양수면 1·아니면 0 입니다.
  • backward 는 손실에서 출발해 기울기 1.0 을 두고, 그래프를 거꾸로 걸으며 "내 기울기 × 국소 미분"을 입력의 기울기에 더합니다(연쇄 법칙).
  • 한 값이 여러 곳에 쓰이면 길마다의 기울기를 모두 더합니다. 그래서 grad 는 = 가 아니라 += 로 쌓습니다.

아래 실험의 y = x² + 2x 는 손으로 미분하면 2x + 2 이고 x = 3 에서 8 입니다. autograd 가 낸 8.0 과, x 를 아주 조금 움직여 잰 숫자 미분 8.0 이 같습니다. c = a·b + a 처럼 a 가 두 번 쓰이면 dc/da = b + 1 = 6 입니다. 두 길의 기울기가 더해졌습니다.

PyTorch 의 loss.backward() 도 같은 일을 합니다. 다만 숫자 하나가 아니라 텐서 단위로 기록하고 계산합니다.

직접 확인하기 lab/grad.py
from engine import Value

x = Value(3)
y = x * x + 2 * x
y.backward()
f = lambda v: v * v + 2 * v
h = 1e-6
print('autograd:', x.grad)
print('numeric :', round((f(3 + h) - f(3 - h)) / (2 * h), 6))
a, b = Value(2), Value(5)
c = a * b + a
c.backward()
print('dc/da =', a.grad, ' dc/db =', b.grad)
cd ~/xslm
mkdir -p lab
micro lab/grad.py
python3 -m lab.grad

실행 결과

autograd: 8.0
numeric : 8.0
dc/da = 6.0  dc/db = 2.0

파라미터 세기 — 1,248개는 어디에 있나

파라미터는 학습이 고치는 숫자 하나하나입니다. 가중치(weight)라고도 부릅니다. XSLM 은 9개의 행렬에 모든 파라미터를 담고, 행렬의 칸 수를 모두 더하면 1,248개입니다. 아래 표와 앞의 임베딩 실험 출력이 같은지 비교해 보세요.

표에서 몇 가지를 읽을 수 있습니다.

  • 어휘에 묶인 token·head 두 표가 640개로 절반을 넘습니다. 어휘를 늘리면 이 두 표가 커집니다.
  • 어텐션 4개(query·key·value·project)가 256개, MLP 2개가 256개입니다. 폭(width)을 2배로 하면 이 부분은 4배가 됩니다.
  • 실제 모델에서는 블록(어텐션 + MLP)이 수십 번 쌓여 파라미터 대부분을 차지합니다.

" 모델"의 B 는 billion, 즉 파라미터가 80억 개라는 뜻입니다. 저장 크기는 파라미터 수 × 숫자 하나의 크기입니다. 실수(4)면 32GB, 16비트면 16GB, 8비트로 줄이면() , 4비트면 4GB 쯤입니다. 실행할 때는 여기에 계산 중의 값과 KV 캐시 가 더 듭니다. XSLM 의 model.json 은 숫자를 글자로 적은 이라 1,248개인데도 약 35KB 입니다.

XSLM 가중치 9종 — 기본 자료(어휘 40개)·width 8·context 12 기준
가중치 이름 모양(행 × 열) 숫자 개수 하는 일
token 40 × 8 320 토큰 번호마다 벡터 한 줄(토큰 임베딩)
position 12 × 8 96 자리마다 벡터 한 줄(위치 임베딩)
query 8 × 8 64 지금 자리가 찾는 것(Query)
key 8 × 8 64 각 자리가 내보이는 특징(Key)
value 8 × 8 64 각 자리가 건네줄 내용(Value)
project 8 × 8 64 어텐션 결과를 다시 정리
expand 16 × 8 128 MLP 앞층 — 8칸을 16칸으로 넓힘
contract 8 × 16 128 MLP 뒤층 — 16칸을 8칸으로 줄임
head 40 × 8 320 어휘마다 다음 토큰 점수(출력층)

XSLM 이 줄인 것 — 실제 GPT 와의 차이

XSLM 은 읽기 쉽게 몇 가지를 빼거나 줄였습니다. GPT-2 Small 과 견주면 이렇습니다.

  • 블록 수: XSLM 1개, GPT-2 Small 12개. 블록을 쌓을수록 더 복잡한 관계를 배웁니다.
  • 어텐션 헤드: XSLM 1개, GPT-2 Small 12개(헤드마다 폭 768 ÷ 12 = 64칸). 여러 헤드가 서로 다른 관계를 동시에 봅니다.
  • MLP: XSLM 은 2배·ReLU, GPT-2 는 4배·GELU.
  • bias 와 정규화의 γ·β: XSLM 에는 없고 GPT-2 에는 있습니다.
  • dropout: XSLM 에는 없습니다. 학습 중 값의 일부(보통 10%)를 무작위로 0으로 만들어 특정 길에 기대지 않게 하는 방지 장치입니다. 생성할 때는 끕니다.
  • 초기값: XSLM 은 표준편차 0.1, GPT-2 는 0.02 의 정규분포.
  • 계산 방식: XSLM 은 숫자 하나씩, PyTorch 는 (묶음 B, 길이 T, 폭 C) 모양의 텐서를 한꺼번에 에서 계산합니다.

빠진 것들은 모두 더 크고 더 안정적으로 학습하기 위한 장치입니다. 다음 토큰 확률을 내는 핵심 계산은 같습니다.

임베딩을 검색용 벡터로만 알면 "GPT 가 다음 낱말과 가장 가까운 벡터를 찾아 고른다"고 오해하기 쉽습니다. 실제로는 이렇습니다.

  • 벡터 검색(RAG 등): 문서들을 벡터로 바꿔 저장해 두고, 질문 벡터와 가까운 문서를 찾아 꺼냅니다. 결과는 이미 있던 문서입니다.
  • GPT 의 예측: 입력 벡터를 블록에서 계산해 바꾼 뒤 출력층이 어휘 전체에 점수를 내고, 그 확률에서 하나를 뽑습니다. 결과는 계산으로 새로 만든 입니다.

어텐션도 검색이 아닙니다. 입력 문장 안의 앞자리들 사이에서 비중을 계산해 섞을 뿐, 바깥 문서를 찾지 않습니다.

둘은 함께 쓰입니다. 실제 서비스는 질문으로 관련 문서를 벡터 검색해 찾고, 그 문서를 에 붙여 GPT 가 답을 생성하게 합니다(RAG). 검색은 최신 정보를, GPT 는 문장 만들기를 맡습니다.

직접 타이핑하며 실습하기

engine.py부터 쓰고 미분 검사를 실행한 뒤 model.py를 작성하세요. Value는 계산마다 해당 연산의 미분값을 기억합니다. backward는 를 거꾸로 따라가며 기울기를 더하므로 같은 가중치를 여러 번 쓴 영향도 합쳐집니다. forward에서 토큰 → 위치 → 어텐션 → MLP → 출력층을 찾아보세요.

cd ~/xslm
micro engine.py
micro model.py
python3 engine.py
python3 -c "from model import GPT; m=GPT(10, 8, 12); print(len(m.forward([1, 2])), len(m.forward([1, 2])[0]))"
터미널에서 실습하기
engine.py직접 작성할 전체 소스

는 숫자, grad는 그 숫자가 손실에 미치는 영향, parents는 연산별 미분 기록입니다. 덧셈은 기울기를 나누어 보내고 곱셈은 상대 입력만큼 곱해 보냅니다. softmax는 점수를 확률로 바꿉니다.

import math


class Value:
    def __init__(self, data, parents=()):
        self.data = float(data)
        self.grad = 0.0
        self.parents = parents

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

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

    def __pow__(self, power):
        return Value(self.data ** power,
                     ((self, power * self.data ** (power - 1)),))

    def __neg__(self):
        return self * -1

    def __sub__(self, other):
        return self + (-other)

    def __truediv__(self, other):
        return self * (other ** -1)

    __radd__ = __add__
    __rmul__ = __mul__

    def exp(self):
        result = math.exp(self.data)
        return Value(result, ((self, result),))

    def log(self):
        return Value(math.log(self.data), ((self, 1 / self.data),))

    def relu(self):
        return Value(max(0, self.data), ((self, float(self.data > 0)),))

    def backward(self):
        order, seen, stack = [], set(), [(self, False)]
        while stack:
            node, expanded = stack.pop()
            if expanded:
                order.append(node)
            elif node not in seen:
                seen.add(node)
                stack.append((node, True))
                stack.extend((parent, False) for parent, _ in node.parents)
        for node in order:
            node.grad = 0.0
        self.grad = 1.0
        for node in reversed(order):
            for parent, derivative in node.parents:
                parent.grad += derivative * node.grad


def softmax(values):
    peak = max(v.data for v in values)
    exps = [(v - peak).exp() for v in values]
    total = sum(exps)
    return [v / total for v in exps]


def linear(x, weights):
    return [sum(a * b for a, b in zip(x, row)) for row in weights]


def norm(x):
    mean = sum(x) / len(x)
    variance = sum((v - mean) ** 2 for v in x) / len(x)
    return [(v - mean) * (variance + 1e-5) ** -0.5 for v in x]


if __name__ == '__main__':
    x = Value(3)
    y = x * x + 2 * x
    y.backward()
    print('y =', y.data, 'dy/dx =', x.grad)
    assert y.data == 15 and x.grad == 8
model.py직접 작성할 전체 소스

shapes에서 크기를 본 뒤 forward를 읽으세요. weights의 Value는 모두 학습 대상입니다. 로 벡터와 행렬을 표현합니다. loss는 각 위치의 다음 토큰과 비교하며 state와 load는 Python 대신 숫자를 저장하고 읽습니다.

import random
from engine import Value, linear, norm, softmax


class GPT:
    def __init__(self, vocab_size, width, context, seed=7):
        self.width, self.context = width, context
        rng = random.Random(seed)
        shapes = {
            'token': (vocab_size, width), 'position': (context, width),
            'query': (width, width), 'key': (width, width),
            'value': (width, width), 'project': (width, width),
            'expand': (2 * width, width), 'contract': (width, 2 * width),
            'head': (vocab_size, width),
        }
        self.weights = {
            name: [[Value(rng.gauss(0, 0.1)) for _ in range(cols)]
                   for _ in range(rows)] for name, (rows, cols) in shapes.items()
        }
        self.parameters = [v for matrix in self.weights.values()
                           for row in matrix for v in row]

    def forward(self, ids):
        assert 0 < len(ids) <= self.context
        w, keys, values, output = self.weights, [], [], []
        for position, token in enumerate(ids):
            x = [a + b for a, b in zip(w['token'][token], w['position'][position])]
            h = norm(x)
            q = linear(h, w['query'])
            keys.append(linear(h, w['key']))
            values.append(linear(h, w['value']))
            scores = [sum(a * b for a, b in zip(q, k)) / self.width ** 0.5
                      for k in keys]
            attention = softmax(scores)
            mixed = [sum(p * v[i] for p, v in zip(attention, values))
                     for i in range(self.width)]
            x = [a + b for a, b in zip(x, linear(mixed, w['project']))]
            hidden = [v.relu() for v in linear(norm(x), w['expand'])]
            x = [a + b for a, b in zip(x, linear(hidden, w['contract']))]
            output.append(linear(norm(x), w['head']))
        return output

    def loss(self, inputs, targets):
        logits = self.forward(inputs)
        losses = [-softmax(row)[target].log()
                  for row, target in zip(logits, targets)]
        return sum(losses) / len(losses)

    def state(self):
        return {name: [[v.data for v in row] for row in matrix]
                for name, matrix in self.weights.items()}

    def load(self, state):
        assert set(state) == set(self.weights), 'checkpoint keys'
        for name, matrix in self.weights.items():
            assert len(state[name]) == len(matrix), 'checkpoint rows'
            for row, saved in zip(matrix, state[name]):
                assert len(saved) == len(row), 'checkpoint columns'
                for value, number in zip(row, saved):
                    value.data = float(number)

코드 한 줄씩 뜯어보기

engine.py Value — 숫자 하나와 그 내력

class Value:
    def __init__(self, data, parents=()):
        self.data = float(data)
        self.grad = 0.0
        self.parents = parents

Value 는 숫자 하나를 감쌉니다. data 는 값, grad 는 손실을 이 값으로 미분한 값(처음 0), parents 는 이 값을 만든 입력들과 그 입력에 대한 국소 미분의 목록입니다. 가중치처럼 처음부터 있는 값은 parents 가 비어 있습니다. float(data) 로 바꿔 두므로 정수를 넣어도 실수로 계산합니다.

engine.py 연산자 — 계산하며 미분을 적어 둔다

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

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

    def __pow__(self, power):
        return Value(self.data ** power,
                     ((self, power * self.data ** (power - 1)),))

    def __neg__(self):
        return self * -1

    def __sub__(self, other):
        return self + (-other)

    def __truediv__(self, other):
        return self * (other ** -1)

    __radd__ = __add__
    __rmul__ = __mul__

Python 은 a + b 를 a.__add__(b) 로 바꿔 부릅니다. 이 특수 메서드들을 정의하면 Value 끼리 보통 수식처럼 계산할 수 있습니다.

  • __add__: 결과 = a + b. 국소 미분은 a 쪽 1, b 쪽 1.
  • __mul__: 결과 = a × b. 국소 미분은 a 쪽 b 값, b 쪽 a 값.
  • __pow__: a 의 n제곱. 국소 미분은 n × a 의 (n − 1)제곱.
  • __neg__·__sub__·__truediv__: 빼기는 −1 곱하기와 더하기로, 나누기는 −1제곱 곱하기로 바꿔 위 세 가지를 다시 씁니다. 새 미분 규칙이 필요 없습니다.
  • other 가 Value 가 아니면(2 같은 보통 수) Value 로 감쌉니다.
  • __radd__·__rmul__: 2 * x 처럼 왼쪽이 보통 수일 때 부릅니다. sum() 은 0 + 첫 값으로 시작하므로 __radd__ 가 있어야 Value 목록을 sum 할 수 있습니다.

engine.py exp·log·relu — 함수의 미분

    def exp(self):
        result = math.exp(self.data)
        return Value(result, ((self, result),))

    def log(self):
        return Value(math.log(self.data), ((self, 1 / self.data),))

    def relu(self):
        return Value(max(0, self.data), ((self, float(self.data > 0)),))

exp 의 미분은 자기 자신이므로 결과값 result 를 그대로 적습니다. log x 의 미분은 1/x 입니다. relu 는 양수면 기울기 1, 0 이하면 0 을 적습니다. float(self.data > 0) 은 참이면 1.0, 거짓이면 0.0 입니다. 이 세 가지와 사칙연산만 있으면 모델 전체의 미분을 구할 수 있습니다.

engine.py backward — 그래프를 거꾸로 걸으며 기울기 모으기

    def backward(self):
        order, seen, stack = [], set(), [(self, False)]
        while stack:
            node, expanded = stack.pop()
            if expanded:
                order.append(node)
            elif node not in seen:
                seen.add(node)
                stack.append((node, True))
                stack.extend((parent, False) for parent, _ in node.parents)
        for node in order:
            node.grad = 0.0
        self.grad = 1.0
        for node in reversed(order):
            for parent, derivative in node.parents:
                parent.grad += derivative * node.grad

두 단계입니다.

  • 순서 만들기(46~54줄): stack 으로 그래프를 깊이 우선으로 탐색해 order 를 만듭니다. 노드가 (노드, False) 로 처음 꺼내지면 seen 에 표시하고 (노드, True) 를 다시 쌓은 뒤 부모들을 쌓습니다. 부모들을 모두 처리한 뒤에 (노드, True) 가 꺼내질 때 order 에 넣으므로, order 에서는 언제나 입력이 결과보다 먼저 옵니다(위상 정렬).
  • 기울기 퍼뜨리기(55~60줄): 그래프 안 모든 노드의 grad 를 0으로 지우고, 손실 자신은 1.0(dL/dL = 1)으로 둡니다. order 를 거꾸로(결과에서 입력으로) 걸으며 부모마다 "국소 미분 × 내 grad" 를 더합니다. 거꾸로 걷기 때문에 한 노드의 grad 가 모든 쓰임에서 다 모인 뒤에야 그 부모로 넘어갑니다.

engine.py softmax — 넘치지 않는 확률

def softmax(values):
    peak = max(v.data for v in values)
    exps = [(v - peak).exp() for v in values]
    total = sum(exps)
    return [v / total for v in exps]

peak 는 점수 가운데 가장 큰 값(.data 로 꺼낸 보통 수)입니다. 모든 점수에서 peak 를 빼고 exp 하므로 가장 큰 값이 1이 되어 넘치지 않습니다. 합 total 로 나누면 합이 1인 확률이 됩니다. peak 는 보통 수로 빼므로 그래프에 기록되지 않지만, 모든 점수에서 같은 수를 빼도 확률은 변하지 않으므로 미분도 그대로 맞습니다.

engine.py linear·norm — 행렬 곱과 정규화

def linear(x, weights):
    return [sum(a * b for a, b in zip(x, row)) for row in weights]


def norm(x):
    mean = sum(x) / len(x)
    variance = sum((v - mean) ** 2 for v in x) / len(x)
    return [(v - mean) * (variance + 1e-5) ** -0.5 for v in x]

linear 는 weights 의 줄(row)마다 x 와 같은 칸끼리 곱해 더한 값을 냅니다. 그래서 weights 는 (출력 칸 수) 줄 × (입력 칸 수) 칸이고, 결과는 줄 수만큼의 칸을 가집니다. PyTorch 의 nn.Linear 도 가중치를 (출력, 입력) 모양으로 저장합니다. norm 은 평균 mean 과 분산 variance 를 구한 뒤 (값 − 평균) ÷ √(분산 + 0.00001) 로 바꿉니다. -0.5 제곱은 제곱근으로 나누기와 같습니다.

engine.py 스스로 검사하기

if __name__ == '__main__':
    x = Value(3)
    y = x * x + 2 * x
    y.backward()
    print('y =', y.data, 'dy/dx =', x.grad)
    assert y.data == 15 and x.grad == 8

engine.py 를 직접 실행하면 y = x² + 2x 를 x = 3 에서 계산하고 미분합니다. y = 9 + 6 = 15, dy/dx = 2·3 + 2 = 8 이어야 하고, assert 가 이것을 확인합니다. 엔진을 고쳤다면 이 검사부터 다시 돌려 보세요.

model.py 가중치 9종 만들기

class GPT:
    def __init__(self, vocab_size, width, context, seed=7):
        self.width, self.context = width, context
        rng = random.Random(seed)
        shapes = {
            'token': (vocab_size, width), 'position': (context, width),
            'query': (width, width), 'key': (width, width),
            'value': (width, width), 'project': (width, width),
            'expand': (2 * width, width), 'contract': (width, 2 * width),
            'head': (vocab_size, width),
        }
        self.weights = {
            name: [[Value(rng.gauss(0, 0.1)) for _ in range(cols)]
                   for _ in range(rows)] for name, (rows, cols) in shapes.items()
        }
        self.parameters = [v for matrix in self.weights.values()
                           for row in matrix for v in row]

shapes 사전이 행렬마다 (줄 수, 칸 수)를 정합니다. 사전 내포가 이름마다 줄 수 × 칸 수 만큼 Value(rng.gauss(0, 0.1)) 를 만듭니다. 평균 0, 표준편차 0.1 의 정규분포에서 뽑은 작은 수입니다. 모두 0으로 채우면 모든 칸이 똑같이 움직여 서로 다른 특징을 배우지 못하므로 무작위로 시작합니다. rng 를 로 만들어 언제나 같은 처음 값이 나옵니다. parameters 는 9개 행렬의 Value 를 한 목록으로 펼친 것으로, 학습이 고칠 숫자 전부입니다.

model.py forward 시작 — 토큰과 위치 더하기

    def forward(self, ids):
        assert 0 < len(ids) <= self.context
        w, keys, values, output = self.weights, [], [], []
        for position, token in enumerate(ids):
            x = [a + b for a, b in zip(w['token'][token], w['position'][position])]
            h = norm(x)

assert 가 입력 길이를 1 이상 context 이하로 제한합니다. 위치 표의 줄 수가 context 라서 더 길면 위치 벡터가 없습니다. keys·values 는 앞자리들의 key·value 를 쌓을 목록입니다. 마다 position(0부터)과 token 번호를 받아, 토큰 벡터와 위치 벡터를 칸마다 더해 x 를 만듭니다. h = norm(x) 는 어텐션에 넣을 정규화한 값입니다(Pre-LN).

model.py 인과 어텐션 한 헤드

            q = linear(h, w['query'])
            keys.append(linear(h, w['key']))
            values.append(linear(h, w['value']))
            scores = [sum(a * b for a, b in zip(q, k)) / self.width ** 0.5
                      for k in keys]
            attention = softmax(scores)
            mixed = [sum(p * v[i] for p, v in zip(attention, values))
                     for i in range(self.width)]
            x = [a + b for a, b in zip(x, linear(mixed, w['project']))]

q 는 지금 자리의 query 이고, 지금 자리의 key·value 는 목록 끝에 더합니다. 목록에는 지금까지의 자리만 있으므로 미래는 볼 수 없습니다. scores 는 q 와 각 key 의 내적 ÷ √width, attention 은 그 softmax, mixed 는 value 들을 attention 비중으로 칸마다 더한 벡터입니다. 마지막 줄에서 mixed 를 project 로 변환해 x 에 더합니다(잔차).

model.py MLP 두 층

            hidden = [v.relu() for v in linear(norm(x), w['expand'])]
            x = [a + b for a, b in zip(x, linear(hidden, w['contract']))]

x 를 정규화해 expand 로 16칸으로 넓히고 칸마다 relu 를 씌운 hidden 을 만듭니다. contract 로 다시 8칸으로 줄여 x 에 더합니다(잔차). 어텐션과 달리 다른 자리를 보지 않고 지금 자리의 벡터만 가공합니다.

model.py 출력층 점수

            output.append(linear(norm(x), w['head']))
        return output

x 를 마지막으로 정규화해 head 와 곱하면 어휘 수만큼의 점수가 나오고, output 에 자리마다 쌓습니다. 반복이 끝나면 output 은 (입력 길이) × (어휘 수) 점수표입니다.

model.py loss — 자리마다 −log 확률의 평균

    def loss(self, inputs, targets):
        logits = self.forward(inputs)
        losses = [-softmax(row)[target].log()
                  for row, target in zip(logits, targets)]
        return sum(losses) / len(losses)

forward 로 자리마다 점수를 얻고, 그 자리의 softmax 확률 가운데 정답 target 의 확률에 log 를 씌워 부호를 바꿉니다. 모든 자리의 손실을 더해 자리 수로 나눈 평균을 돌려줍니다. 결과도 Value 라서 loss.backward() 를 부를 수 있습니다.

model.py state·load — 가중치 저장과 읽기

    def state(self):
        return {name: [[v.data for v in row] for row in matrix]
                for name, matrix in self.weights.items()}

    def load(self, state):
        assert set(state) == set(self.weights), 'checkpoint keys'
        for name, matrix in self.weights.items():
            assert len(state[name]) == len(matrix), 'checkpoint rows'
            for row, saved in zip(matrix, state[name]):
                assert len(saved) == len(row), 'checkpoint columns'
                for value, number in zip(row, saved):
                    value.data = float(number)

state 는 Value 대신 보통 수(.data)만 꺼낸 같은 모양의 사전을 돌려줍니다. Value 객체에는 계산 그래프 연결이 있어 JSON 으로 저장할 수 없기 때문입니다. load 는 반대로 저장한 숫자를 Value 의 data 에 넣습니다. 그 전에 행렬 이름·줄 수·칸 수가 지금 모델과 같은지 assert 로 확인해, 다른 크기의 체크포인트를 조용히 잘못 읽지 않게 합니다.

여기까지 이해했나요?

engine.py는 y = 15.0, dy/dx = 8.0을 출력해야 합니다. 모델 크기 검사는 2 10을 출력합니다. 두 위치마다 어휘 10개의 점수를 냈다는 뜻입니다. 뒤에 토큰을 더해도 앞자리 점수가 바뀌면 안 되는 이유를 설명하세요.

막혔을 때 확인하기

행렬의 행 크기와 zip으로 묶는 벡터 길이를 확인하세요. 단어를 세는 으로 이 층들을 대신하지 않습니다. 작지만 임베딩·어텐션·MLP·출력층의 모든 가중치를 역전파로 학습합니다.

자주 묻는 질문

가중치와 벡터는 같은 건가요?

역할과 모양의 차이입니다. 가중치(파라미터)는 학습이 고치는 숫자라는 역할을 말하고, 벡터는 숫자가 한 줄로 늘어선 모양을 말합니다. 임베딩 표 전체가 가중치이고, 그 표에서 꺼낸 한 줄이 토큰의 벡터입니다. 계산 중에 생기는 q·k·v 같은 벡터는 가중치가 아니라 그때그때의 중간값입니다.

지식은 모델의 어디에 저장되나요?

정해진 칸에 문장이 들어 있지 않습니다. 사진이 하나가 아니라 모든 픽셀의 배치로 그림이 되듯, 지식은 모든 가중치가 함께 만든 계산 습관에 흩어져 있습니다. 그래서 특정 사실 하나만 골라 지우거나 고치기가 어렵습니다.

왜 √폭으로 나누나요?

폭이 d 인 무작위 벡터 두 개의 내적은 d 가 클수록 크게 흔들립니다(표준편차가 √d 배). 점수가 크면 softmax 가 거의 한 자리에만 1을 주어 다른 자리로 기울기가 흐르지 않습니다. √d 로 나누면 폭과 상관없이 점수의 크기가 비슷하게 유지됩니다.

backward 는 왜 재귀 대신 stack 을 쓰나요?

계산 그래프는 학습 한 번에 3만 개가 넘는 노드가 길게 이어집니다. 재귀 함수로 따라가면 Python 의 재귀 한도(기본 1,000단계)를 넘어 RecursionError 가 날 수 있습니다. stack 으로 순서를 직접 관리하면 그래프가 깊어도 안전하게 위상 정렬(입력이 먼저 오는 순서)을 만들 수 있습니다.

가이드라인

인공지능

에이전트 툴

바이브코딩

AI 활용

AI 자격증

화상 수업

IT 기초

채팅

게시판

만들기

소통

더보기

용어 사전

모르는 말을 골라 보세요. 쉬운 설명과 관련 용어를 볼 수 있어요.

찾는 용어가 아직 없어요.

용어를 고르면 설명이 보여요.