Profile image
Jinyoung
Dev

TIL-05: Deep Learning 01

20% Human
80% AI
TIL-05: Deep Learning 01
0 views
20 min read

Andrej Karpathy의 nanochat을 공부해보기 위해 사전 단계로 아주 기초적인 것부터 정리해봅니다.

출발점: 왜 머신러닝이 필요한가?

전통 프로그래밍의 한계에서 시작해봅시다.

개발자로서 우리가 아는 세계

우리는 함수를 만들 때 규칙을 직접 코드로 작성합니다. 이건 당연한 일이죠.

// 전통 프로그래밍: 사람이 규칙을 작성
int celsiusToFahrenheit(int celsius) {
  return celsius * 9 / 5 + 32;

사람이 규칙(코드)을 작성 → 컴퓨터가 데이터 처리

입력 → 규칙 적용 → 출력. 명확하고 예측 가능합니다. 그런데...

규칙을 작성할 수 없는 문제

만약 이런 함수를 만들어야 한다면 어떨까요?

// 이 함수의 규칙을 직접 작성할 수 있을까?
String whatAnimalsIsThis(Image photo) {
  // ??? 픽셀 값으로 고양이를 어떻게 구분하지?
  return "cat"; // 또는 "dog"
}

"귀가 뾰족하면 고양이"라고 규칙을 정할 수도 있겠지만, 강아지 중에도 귀가 뾰족한 경우가 있고, 고양이 중에도 귀가 둥근 경우가 있습니다. "털이 짧으면?" "수염이 있으면?" ... 규칙은 끝없이 복잡해지고, 예외는 계속 발생합니다.

머신러닝의 핵심 아이디어

규칙을 사람이 직접 작성하는 대신,
'정답이 딸린 예시 데이터'를 대량으로 보여주고, 컴퓨터가 스스로 규칙을 찾게 한다.

사람이 데이터를 제공컴퓨터가 규칙을 발견

  • 고양이 사진 10만장 + 라벨
  • 데이터 양과 질에 의존

컴퓨터가 "규칙을 발견"하는 구체적인 메커니즘을 Neural Network(신경망)이라고 부릅니다.


1. 뉴런(Neuron): 가장 작은 단위

뉴런이란?

신경망(Neural Network)은 뉴런(neuron)이라는 작은 단위들이 모여서 이루어집니다. 뉴런 하나가 하는 일은 놀랍도록 단순합니다.

뉴런이 하는 일:

  1. 입력들을 받아서
  2. 각각에 가중치를 곱하고
  3. 모두 더한 뒤
  4. 활성화 함수를 통과시킨다

이것을 의사 코드로 나타내면 이렇습니다:

// 뉴런 하나가 하는 일의 전부
double neuron(double[] inputs, double[] weights, double bias) {
  double sum = bias;
  for (int i = 0; i < inputs.length; i++) {
    sum += inputs[i] * weights[i]; // 입력 x 가중치
  }
  return activate(sum); // 활성화 함수 통과
}

숫자로 따라가기

구체적인 예시를 봅시다. 동물 분류 뉴런이 입력 2개를 받는다고 가정합니다:

인터랙티브 — 뉴런 계산 따라가기

슬라이더를 움직여서 가중치와 편향이 결과를 어떻게 바꾸는지 관찰해보세요.

입력 x₁ (몸무게 kg)4.0
입력 x₂ (키 cm)25.0
가중치 w₁0.5
가중치 w₂-0.3
편향 b1.0
x₁ × w₁4.0 × 0.5 = 2.00
x₂ × w₂25.0 × -0.3 = -7.50
합산 + bias2.00 + -7.50 + 1.0 = -4.50

tanh(-4.50)출력 = -0.9998

강한 음성 신호

핵심 용어 정리

용어개발자 비유설명
가중치(Weight)설정값 변수각 입력이 얼마나 중요한지를 나타내는 숫자. 학습 대상!
편향(Bias)기본 오프셋입력이 전부 0이어도 뉴런이 가지는 기본값
활성화 함수if문 같은 것결과값의 범위를 조절. 예: ReLU(음수 → 0), tanh( -1 ~ 1)
파라미터튜닝 다이얼가중치 + 편향을 통칭. 학습이 조정하는 대상

뉴런의 핵심 연산은 곱하기와 더하기, 그리고 활성화 함수 적용이 전부입니다. 하지만 이걸 수백만 개 연결하면 놀라운 일이 일어납니다. 이것을 신경망이라고 합니다.


2. 신경망(Neural Network): 뉴런의 조합

간단한 부품을 층층이 쌓아 복잡한 패턴을 잡아내는 구조

레이어(Layer)란?

뉴런 하나로는 "몸무게가 가벼우면 고양이"처럼 아주 단순한 판단만 할 수 있습니다. 복잡한 판단을 하려면 뉴런을 **층(Layer)**으로 묶어서 쌓아야 합니다.

개발자 비유: 파이프라인
신경망은 데이터 처리 파이프라인과 같습니다.

입력 데이터 → Layer 1 → Layer 2 → Layer 3 → 최종 출력

Spring의 Filter Chain처럼, 각 Layer가 데이터를 변환해서 다음 Layer에 전달합니다.
차이점은 각 필터의 규칙이 하드코딩이 아니라 **학습으로 결정된다**는 것.

3가지 레이어 종류

레이어역할비유
입력 레이어원본 데이터를 받음API의 Request Body
은닉 레이어패턴을 추출/변환내부 서비스 로직(여러 개 쌓을 수 있음)
출력 레이어최종 예측 결과 반환API의 Response Body

왜 "깊게" 쌓는가? (Deep의 의미)

"딥러닝(Deep Learning)"의 "딥"은 은닉 레이어가 여러개라는 뜻입니다.

층이 깊을수록 복잡한 패턴을 잡는다:

  • Layer 1: 아주 단순한 특징 ("이 부분은 밝다", "여기에 직선이 있다")
  • Layer 2: 조금 더 복잡한 특징 ("이건 둥근 모양이다", "이건 줄무늬다")
  • Layer 3: 고수준 특징 ("이건 눈이다", "이건 귀다")
  • Layer 4: 최종 판단 ("이건 고양이다")

각 Layer가 이전 Layer의 결과를 조합해서 점점 더 추상적인 개념을 만들어냅니다.

파라미터 수 = 뉴런 수 x 연결 수

간단한 예로 계산해봅시다:

// 아주 작은 신경망
입력 레이어: 3개 뉴런
은닉 레이어: 4개 뉴런
출력 레이어: 2개 뉴런

// 파라미터 수
Layer1→Layer2: 3 × 4 = 12 weights + 4 biases = 16
Layer2→Layer3: 4 × 2 = 8 weights + 2 biases = 10
────────────────────────
총 파라미터: 26개

// GPT-2 (nanochat d26): 약 5.68억 개

3. Forward Pass: 입력에서 출력까지

데이터가 신경망을 통과하는 과정

Forward Pass란?

입력 데이터를 신경망에 넣고, 레이어를 한 층 한 층 거쳐서 최종 출력(예측값)을 얻는 과정을 **Forward Pass(순전파)**라고 합니다. "앞으로 쭉 통과한다"는 뜻이에요.

개발자 비유:

HTTP 요청이 Controller → Service → Repository를 거쳐 응답이 나오는 것처럼,
데이터가 Input Layer → Hidden Layers → Output Layer를 거쳐 예측이 나옵니다.

숫자로 따라가기: 미니 신경망

아주 작은 신경망으로 Forward Pass를 처음부터 끝까지 계산해봅시다.

문제 설정:

목표: 시험 점수를 2개를 넣으면 합격/불합격을 예측하는 신경망
구조: 입력 2개 → 은닉 뉴런 2개 → 출력 1개
활성화 함수: ReLU(음수는 0으로, 양수는 그대로)

인터랙티브 — Forward Pass 전체 과정

입력 점수를 바꿔보면서 신경망의 예측이 어떻게 변하는지 관찰하세요.

수학 점수 (x₁)80
영어 점수 (x₂)70

입력 정규화: x₁=0.80, x₂=0.70

은닉 레이어
뉴런 h₁ = ReLU(0.80×0.6 + 0.70×0.4 - 0.3)0.4600
뉴런 h₂ = ReLU(0.80×(-0.2) + 0.70×0.8 - 0.1)0.3000

출력 레이어
sigmoid(0.460×0.7 + 0.300×0.5 - 0.2)0.5676
예측: 56.8% 확률로 합격 ✅

Forward Pass는 순수한 **수학 연산(곱셈과 덧셈의 연쇄)**입니다. 입력 숫자가 레이어를 거치며 변환되어 최종 예측이 나옵니다.

하지만 위 예시의 가중치(0.3, -0.1 등)는 제가 임의로 넣은 값입니다. 이 가중치들을 "올바른 값"으로 조정하는 것이 바로 학습입니다. 그리고 학습의 핵심 알고리즘이 바로 Backpropagation입니다.


4. Loss: 얼마나 틀렸는가?

Backpropagation을 이해하기 전에, 먼저 "틀린 정도"를 측정하는 법

왜 Loss가 필요한가?

신경망이 예측을 했습니다. 이제 **"얼마나 잘 맞췄는지"**를 숫자로 측정해야 합니다. 이 숫자를 **Loss(손실)**라고 부릅니다.

개발자 비유:

테스트 코드에서 assertEquals(expected, actual)이 실패하면 에러 메시지가 나오죠.
Loss는 이 "얼마나 다른지"를 숫자 하나로 표현한 것입니다.

Loss = 0 → 완벽하게 맞춤 (테스트 통과)
Loss = 큰 수 → 크게 틀림 (테스트 실패)

가장 간단한 Loss: 차이의 제곱

Loss = (예측값 - 정답)²

(Squared Error, 제곱 오차)

왜 단순히 빼지 않고 제곱을 할까?

예측정답차이(예측-정답)제곱(Loss)
0.81.0-0.20.04
0.31.0-0.70.49
1.21.0+0.20.04
-0.51.0-1.52.25

제곱을 하면 두 가지가 해결됩니다:

  • ① 방향(+/-)에 관계없이 항상 양수
  • ② 크게 틀릴수록 페널티가 훨씬 커짐. 0.2 차이는 Loss 0.04이지만, 1.5 차이는 Loss 2.25로 급격히 커집니다.

핵심 포인트: 학습의 목표는 단 하나, Loss를 최소화 하는 것

Loss가 줄어든다 = 예측이 정답에 가까워진다 = 모델이 학습하고 있다.

nanochat에서 모니터링하는 val_bpb(validation bits per byte)가 바로 Loss의 일종입니다.

자, 이제 준비가 됐습니다

우리가 알게 된 것을 정리하면:

  1. Forward Pass로 예측값을 구한다
  2. Loss로 "얼마나 틀렸는지"를 측정한다
  3. 이제 남은 질문: "Loss를 줄이려면 각 가중치를 어떻게 조정해야 하는가?"

이 질문에 답하는 알고리즘이 바로 Backpropagation입니다.


5. Backpropagation: 오차 역전파

Loss를 줄이기 위해 "각 가중치를 어느 방향으로 조정할지" 알아내는 알고리즘

BACKPROPAGATION이 답하는 질문

가중치 w를 아주 조금 바꾸면, Loss는 얼마나, 어느 방향으로 변하는가?

이 답을 알면, Loss가 줄어드는 방향으로 가중치를 조정할 수 있습니다. 이 "얼마나, 어느 방향으로 변하는가"를 그래디언트(gradient, 기울기)라고 부릅니다.

비유: 눈 감고 산에서 내려오기

상상해봅시다. 눈을 감고 산 위에 서 있습니다. 가장 낮은 곳(Loss = 0)으로 내려가야 합니다.

산에서 내려오는 전략 = GRADIENT DESCENT
1. 발바닥으로 경사를 느낀다 → "이 방향이 내리막이구나" (= gradient 계산)
2. 내리막 방향으로 한 걸음 내딛는다 (= 가중치 업데이트)
3. 다시 경사를 느끼고, 또 한 걸음 (= 반복)
4. 더 이상 내려갈 곳이 없다 → 도착! (Loss 최소화)

"발바닥으로 경사를 느끼는 것" = Backpropagation
"내리막으로 걸어가는 것" = Gradient Descent (경사 하강법)

Gradient란?

수학적으로 gradient는 "미분"이지만, 직관적으로 이해할 수 있습니다:

인터랙티브 — Gradient 직관 이해하기Loss = (w × 3 - 7)² 에서 w를 움직여보세요. gradient는 'w를 키우면 Loss가 어떻게 변하는지'를 알려줍니다.
가중치 w1.0
예측 = w × 31.0 × 3 = 3.0
정답7
Loss = (3.0 - 7)²16.00

Gradient (dLoss/dw)-24.0
→ w를 키워야 Loss 감소

Chain Rule: Backpropagation 의 핵심 원리

실제 신경망에서는 가중치 w가 여러 연산을 거쳐 최종 Loss에 영향을 미칩니다. 마치 도미노와 같습니다.

w → (w × x) → (합산) → (활성화) → (다음 레이어) → ... → Loss

"w가 변하면 Loss가 얼마나 변하는가?"를 알려면 이 도미노 체인의 각 단계가 다음 단계에 미치는 영향을 곱해 나가면 됩니다. 이게 Chain Rule(연쇄 법칙)입니다.

개발자 비유: 에러 추적

Production에서 에러가 발생했습니다 (= Loss가 큼)
Stack trace를 따라가며 에러의 원인을 추적합니다.

Error in server logs (출력)
← caused by: Service Layer (은닉 레이어)
  ← caused by: wrong DB query (가중치)

Backpropagation도 동일합니다. Loss(에러)에서 시작해서 뒤로(Back) 한 층씩 거슬러 올라가며 "누구의 책임(gradient)이 큰지" 추적합니다.

그래서 이름이 Back-propagation(역전파)인 것입니다.

아주 작은 Backpropagation

아주 단순한 예시로 전체 과정을 숫자로 따라가봅시다.

문제 설정:

뉴런 1개. 입력 x = 2, 가중치 w = 3, 편향 b = 1
예측 = w * x + b, 정답 = 10, Loss = (예측 - 정답)²
인터랙티브 — Backpropagation 전체 과정w와 b를 바꿔가며 Forward → Loss → Backward 전체 흐름을 관찰하세요. 목표: Loss를 0에 가깝게 만들 수 있는 w, b를 찾아보세요!
가중치 w3.75
편향 b3.50
① Forward Pass
예측 = w×x + b = 3.75×2 + 3.5011.00
② Loss 계산
Loss = (11.00 - 10)²1.00
③ Backward Pass (Chain Rule)
dLoss/d예측 = 2×(11.00 - 10)2.00
d예측/dw = x = 22
dLoss/dw = 2.00 × 24.00
dLoss/db = 2.00 × 12.00
④ 업데이트 (lr=0.01)
새 w = 3.75 - 0.01×4.003.7100
새 b = 3.50 - 0.01×2.003.4800

이제부터 조금씩 어려워지기 시작합니다... 하지만 좀 더 힘을 내봅시다!

제가 이해한 Chain Rule은 다음과 같습니다:

[목표] Loss를 줄이고 싶다
        ↑
[질문] w를 움직이면 Loss가 얼마나 변하나? = dLoss/dw
        ↑
[분해] w가 직접 Loss를 바꾸는 게 아니라, 체인을 거친다:

    w를 움직이면 → 예측이 변하고 → 예측이 변하면 → Loss가 변한다

    dLoss      dLoss     d예측
    ─────   = ─────── × ──────
     dw        d예측       dw

내가 궁극적으로 알고 싶은 것은 `dLoss/dw` 이지만 이 값을 한번에 구하기 어려우니 중간 단계 2가지를 먼저 구하고 이 값들을 곱하면 되는 것입니다.
  - dLoss/d예측
  - d예측/dw

그래서 dLoss/dw를 구하기 위해서는 dLoss/d예측d예측/dw를 구해야 합니다. 이 2가지 중간값들을 각각 계산해봅시다.

이 때 사용할 상황:

w = 3.75, b = 3.50
1) Forward pass
예측 = 3.75 * 2 + 3.50 = 11.00
2) Loss 계산
Loss = (11.00 - 10.00)² = 1.00

중간 단계1: dLoss/d예측

dLoss/d예측 = 예측값(Forward pass 계산)이 얼마나 변해야 Loss 값이 얼마만큼 변할 것인가?

"예측값을 살짝 움직이면 Loss가 얼마나 변하는지" 실험해봅시다. 현재 예측값은 11.00, 정답은 10입니다.

실험 1: 0.05만큼 움직여보기

  • 기존 예측값: 11.00 → Loss = (11.00 - 10)² = 1.0000
  • 새 예측값: 11.05 → Loss = (11.05 - 10)² = 1.1025
  • Loss 변화량: 0.1025
  • 변화 비율: 0.1025 / 0.05 = 2.05

실험 2: 더 작게, 0.001만큼 움직여보기

  • 기존 예측값: 11.00 → Loss = 1.000000
  • 새 예측값: 11.001 → Loss = (11.001 - 10)² = 1.002001
  • Loss 변화량: 0.002001
  • 변화 비율: 0.002001 / 0.001 = 2.001

간격을 줄일수록 변화 비율이 2.0에 수렴하는 게 보이시나요?

이 "간격을 극한까지 줄였을 때의 변화 비율"이 바로 미분이고, 이 값이 gradient입니다. 이 경우 dLoss/d예측 = 2.0 이 됩니다.

중간 단계2: d예측/dw

d예측/dw = 가중치 w가 변하면 예측값이 얼마나 변하는가?

이건 훨씬 단순합니다. 예측 = w × 2 + b 이니까, w를 움직여봅시다.

  • w = 3.75 → 예측 = 3.75 × 2 + 3.50 = 11.00
  • w = 3.76 → 예측 = 3.76 × 2 + 3.50 = 11.02
  • 예측 변화량: 0.02
  • 변화 비율: 0.02 / 0.01 = 2.0

w를 0.01 움직였더니 예측이 0.02 변했습니다. w를 얼마를 움직이든 비율은 항상 2.0입니다. 왜냐하면 예측 = w × 2 + b에서 w에 붙은 계수가 x = 2이기 때문입니다.

즉, d예측/dw = x = 2.0

최종 계산: dLoss/dw

이제 두 조각을 합칠 차례입니다.

    dLoss      dLoss     d예측
    ─────   = ─────── × ──────
     dw        d예측       dw

    dLoss/dw = 2.0 × 2.0 = 4.0

즉, w를 1만큼 키우면 Loss는 4만큼 커집니다. w를 1만큼 줄이면 Loss는 4만큼 작아집니다.

여기서 w를 너무 크게 조정하기 보다는 0.01 단위(lr)로 조정하는 것이 좋겠습니다.

그러면 이제 새로운 가중치(w)는 다음과 같이 계산 가능합니다:

  • 신규 w = 기존 w - lr(0.01) x dLoss/dw
  • 신규 w = 3.75 - 0.01 x 4.0
  • 신규 w = 3.71

또 다른 계산: 편향 b

아직 계산이 끝난 것이 아닙니다. 편향 b도 같은 방식으로 계산해야 합니다.

d예측/dw와 같은 방식으로 하면 됩니다. 예측 = w × 2 + b에서 b에 붙은 계수는 1이니까:

  • b를 0.01 움직이면 → 예측도 정확히 0.01 변함
  • 변화 비율: 항상 1.0
  • d예측/db = 1.0

그리고 dLoss/db = dLoss/d예측(앞에서 계산한 값 재사용) × d예측/db = 2.0 × 1.0 = 2.0

따라서 신규 b는 다음과 같이 계산 가능합니다:

  • 신규 b = 기존 b - lr(0.01) x dLoss/db
  • 신규 b = 3.50 - 0.01 x 2.0
  • 신규 b = 3.48

Backpropagation 최종 정리

왜 gradient를 구할 때, w와 b를 각각 따로 계산하는가?

  • 예측값(Forward pass)에 미치는 영향의 정도가 다르기 때문입니다.
  • w는 input x값과 곱한 값이 예측값에 더해지지만 b는 그냥 더해지기 때문입니다.
  • 즉, w는 x값에 비례해서 예측값에 영향을 주지만 b는 1만큼 더해지기 때문에 예측값에 1만큼 영향을 줍니다.

Chain Rule을 사용하면 좋은 점은 무엇인가?

  • 한 번에 구하기 어려운 걸, 쉬운 조각으로 나눠서 구한다는 것입니다.
  • dLoss/dw를 직접 구하려면 복잡하지만 dLoss/d예측과 d예측/dw를 각각 따로 계산한 후 곱하면 되기 때문입니다.
  • 이걸 신경망 규모로 확장해보면 장점이 명확해집니다.
    • 레이어가 26개여도 각 레이어의 gradient는 단순한 곱셈으로 계산 가능합니다.
    • 뒤에서부터 한 층씩 곱해 나가면 전체 gradient가 구해집니다.

Backpropagation의 핵심 아이디어

  • Chain Rule을 "뒤에서 앞으로" 적용하는 전략
  • 계산 효율성
    • 출력에서 시작해서 뒤로 가면, 중간 gradient를 재사용할 수 있습니다.
      • 예: dLoss/d예측을 한 번 구하면, dLoss/dw와 dLoss/db 둘 다에 재활용
    • 반대로 앞에서부터 가면? 파라미터마다 처음부터 다시 계산해야 합니다.
    • 파라미터 개수가 5억개라면? 계산량 차이가 어마어마할 것입니다.

6. 학습 루프

Forward Pass + Loss + Backpropagation을 합치면 "학습"이 됩니다.

Training Loop = 반복이 전부

지금까지 배운 것을 하나로 합치면, 신경망의 학습은 놀랍도록 단순한 루프입니다:

1. Forward Pass
  : 데이터를 넣고 예측값을 계산
2. Loss 계산
  : 예측이 정답과 얼마나 다른지 측정
3. Backward Pass (Backpropagation)
  : Loss에서 거슬러 올라가며 각 파라미터의 gradient를 계산
4. 파라미터 업데이트
  : gradient 반대 방향으로 파라미터를 살짝 조정
5. 1번으로 돌아가서 반복
  : 수만~수십억 번 반복하면 점차 Loss가 줄어듦

이것을 의사코드로 표현하자면:

for (step = 0; step < 1_000_000; step++) {

  // 1. Forward: 예측
  prediction = model.forward(input_data);

  // 2. Loss: 얼마나 틀렸나?
  loss = (prediction - target) ** 2;

  // 3. Backward: 각 파라미터의 gradient 계산
  gradients = backpropagate(loss);

  // 4. Update: gradient 반대 방향으로 조정
  for each param in model.parameters:
    param -= learning_rate * gradients[param];
}

실제 규모 체감하기

위 의사코드NANOCHAT GPT-2
파라미터 수수십 개~5.68억 개
학습 데이터몇 개~45억 토큰(웹페이지 텍스트)
반복 횟수수천 번수만 스텝(각 스텝 = 50만 토큰)
소요 시간1초~1.65시간(8xH100 GPU)
비용0원~$48

7. LLM과 nanochat의 연결

지금까지 배운 것이 실제 LLM에서 어떻게 적용되는지

LLM = "다음 단어 예측"에 최적화된 거대 신경망에

지금까지 배운 모든 개념이 LLM에 그대로 적용됩니다. 달라지는 건 **"무엇을 예측하느냐"**뿐입니다.

LLM 학습 루프:

1. Forward: "The sky is" → 신경망 통과 → 다음 단어 확률 분포 예측
2. Loss: 정답 "blue"와 비교. "blue"의 확률이 낮았으면 Loss 큼
3. Backward: 5.68억 개 파라미터 각각의 gradient 계산
4. Update: "blue"의 확률이 높아지는 방향으로 파라미터 조정
5. 반복: 45억 토큰에 대해 이걸 반복

이걸 끝까지 하면 → "영어를 이해하는" 신경망 완성

nanochat 코드와 매핑

개념NANOCHAT 파일역할
신경망 구조gpt.pyTransformer 모델 정의(뉴런, 레이어 구성)
Forward Passgpt.pyforward()입력 토큰 → 다음 토큰 확률 계산
Loss 계산base_train.pyCross-entropy loss (분류 문제용 Loss)
BackpropagationPyTorch의 .backward()자동으로 gradient 계산(autograd)
파라미터 업데이트optim.pyAdamW + Muon 옵티마이저
학습 데이터dataloader.pyClimbMix 데이터 로딩
추론(생성)engine.py학습된 모델로 한 토큰씩 생성

Andrej Karpathy의 nanochat 모듈을 그대로 사용해서 제가 직접 한땀 한땀 모델을 만들어보려고 합니다.

앞으로 nanochat의 코드를 하나씩 분석해보고, 직접 코드를 돌려보면서 필요한 다른 개념을 추가적으로 공부해나가도록 할 예정입니다. 관련 모든 자료는 블로그를 통해 공개하겠습니다.

Comments (0)

Checking login status...

No comments yet. Be the first to comment!