모델은 어떻게 학습하는가

21
  • #AI
  • #Backpropagation
  • #Loss Function
  • #Gradient Descent

이전 글에서는 AI의 전반적인 개념과 발전 과정을 다뤘다. 이번 글은 더 나아가 모델이 어떻게 학습하는지, 그중에서도 개인적으로 헷갈렸던 개념인 손실함수와 역전파를 좀 더 깊게 정리해 볼 예정이다.

학습이란

AI에서 모델은 입력을 받아 출력을 내놓는 함수다. 그 함수의 동작은 안에 들어 있는 수많은 숫자(파라미터, 가중치 WW와 편향 bb)에 따라 정해진다.

모델은 파라미터로 가득 찬 함수 — 입력을 받아 함수를 거쳐 출력을 낸다
모델 = 입력 → 함수(파라미터) → 출력

학습은 그 숫자들을 계속 미세하게 조정해서 모델의 출력이 정답에 가까워지도록 만드는 과정이다. 출력이 숫자(회귀 — 예: 집값)라면 정답값과의 차이를, 카테고리(분류 — 예: 고양이/강아지)라면 모델이 정답 카테고리에 매긴 확률이 1에 얼마나 가까운지를 본다. 그 차이를 줄이는 방향으로 다시 숫자들을 미세하게 조정하고, 이걸 반복한다.

학습 — 예측이 정답에 가까워지고, 확률이 정답 카테고리에 쏠리도록
학습 — 예측과 확률을 정답 쪽으로

한 줄로 간단하게 정리하자면 "학습은 예측과 정답의 차이를 줄이는 방향으로 파라미터를 조금씩 조정하는 일을, 데이터를 한 묶음씩 보면서 반복한다."

그 한 묶음마다 네 단계를 거친다.

  • 순전파(forward) — 모델이 입력으로 예측을 만든다
  • 손실(loss) — 정답과 예측의 차이를 숫자 하나로 나타낸다
  • 역전파(backward) — "각 파라미터를 어디로 조정하면 손실이 줄어들까?"를 모든 파라미터에 대해 계산한다 (= gradient)
  • 업데이트(update) — 그 방향으로 파라미터를 학습률만큼 움직인다
학습 루프 한 사이클 — batch 하나가 forward → loss → backward → update 네 단계를 거치고, 이걸 데이터 전체로 한 바퀴 돌면 1 epoch
학습 루프 — 한 step과 epoch

한 step에서 실제로 일어나는 일을 보면 batch 안 모든 데이터(예: 32개)를 한꺼번에 모델에 넣어 예측 32개를 만들고, 각 데이터의 손실을 따로 계산한 뒤 평균낸 값(= batch 손실) 하나로 파라미터를 한 번만 조정한다. 32개를 32번 따로 업데이트하는 게 아니라, 평균 손실에 대해 한 걸음만 움직인다는 뜻이다.

이 사이클을 데이터 전체에 대해 여러 번 돌리면 (한 바퀴를 epoch라고 부른다) 모델의 예측값이 점점 정답에 가까워진다.

데이터 준비

학습 사이클을 돌리기 전에, 먼저 데이터를 모델이 받을 수 있는 형태로 준비해야 한다. 보통 다음과 같은 순서를 거친다.

  1. 수집·정제 — 데이터 모으고 깨진 값·중복 걷어내기
  2. 분할 — train(학습 데이터 70%) / validation(검증 데이터 15%) / test(평가 데이터 15%)로 나누기
  3. 정규화 — 변수 간 숫자 스케일 맞추기
  4. 인코딩 — 텍스트·카테고리를 숫자 벡터로 바꾸기
  5. 배치화 — 묶음(batch) 단위로 쪼개기

실제 프로젝트에선 모델 코드를 짜는 것보다 데이터 준비에 손이 더 많이 간다고 한다. 다만 공부하거나 빠르게 검증이 필요할 땐 Hugging Face의 datasets 라이브러리에서 공개 데이터셋을 가져와 실험해 볼 수 있다.

from datasets import load_dataset
 
# 캘리포니아 집값 데이터셋 (1990년 인구조사 기반, 회귀 task)
ds = load_dataset("gvlassis/california_housing")
 
print(ds)
# DatasetDict({
#     train:      Dataset({ features: [...], num_rows: 16640 }),
#     validation: Dataset({ features: [...], num_rows: 2000 }),
#     test:       Dataset({ features: [...], num_rows: 2000 }),
# })
 
print(ds["train"][0])
# {'MedInc': 8.3252, 'HouseAge': 41.0, ..., 'MedHouseVal': 4.526}

순전파

순전파는 지금 가진 파라미터로 모델이 어떤 답을 내는지 계산하는 단계다. 데이터가 입력에서 출력으로 한 방향으로 흐른다고 해서 순전파(forward propagation) 라고 부른다.

가장 단순한 선형 모델이라면:

y^=wx+b\hat{y} = w x + b

입력 xxww를 곱하고 bb를 더하면 끝이다. 파라미터는 ww, bb 두 개 — 이 두 숫자가 모델의 동작을 결정한다.

import numpy as np
 
w, b = 2.0, 1.0                    # 파라미터
x    = np.array([1.0, 2.0, 3.0])   # 입력 3개
 
y_hat = w * x + b                  # 순전파
print(y_hat)                       # [3. 5. 7.]

신경망은 이 변환을 여러 층 쌓고 사이에 비선형(ReLU 같은 함수)을 끼운 형태다. 그 덕에 직선뿐 아니라 곡선 패턴까지 학습할 수 있다.

가장 단순한 2-layer 신경망이라면:

y^=W2ReLU(W1x+b1)+b2\hat{y} = W_2 \, \text{ReLU}(W_1 x + b_1) + b_2

선형 변환 → 비선형(ReLU) → 선형 변환 을 차례로 거친다. 층을 더 쌓고 싶으면 같은 패턴(선형 → 비선형)을 반복하면 된다.

신경망 순전파 — 입력 x가 여러 layer를 거쳐 예측 ŷ에 도달한다
신경망 순전파 — 데이터가 앞으로 흐른다
import numpy as np
 
# 2-layer 신경망: x → ReLU(W1·x + b1) → W2·a1 + b2 = ŷ
x  = np.array([1.0, 2.0])
 
W1 = np.array([[0.5, -0.3],         # 1층 가중치 (3 × 2)
               [0.8,  0.2],
               [-0.1, 0.4]])
b1 = np.array([0.1, -0.2, 0.05])
 
W2 = np.array([0.7, -0.5, 0.3])     # 2층 가중치 (1 × 3)
b2 = 0.1
 
z1    = W1 @ x + b1                 # 1층 선형
a1    = np.maximum(0, z1)           # ReLU (비선형)
y_hat = W2 @ a1 + b2                # 2층 선형 → 출력
 
print(y_hat)                        # -0.175

학습 시작 전 WW, bb는 랜덤한 숫자다. 그래서 처음 순전파의 예측값은 정답과 한참 동떨어져 있는 경우가 많다. 학습이 진행되며 WW, bb가 조금씩 조정되면서 비로소 정답에 가까워진다.

손실함수

순전파가 예측 y^\hat{y}을 내놨다. 정답은 yy. 여기서 데이터 하나에 대한 정답과 예측이 얼마나 다른지를 숫자 하나로 나타내는 게 손실함수(loss function)의 역할이다.

손실함수 — 정답 y와 예측 ŷ을 숫자 하나로 줄여 준다
손실함수 — 두 값을 숫자 하나로

입력은 두 개(정답, 예측), 출력은 양수 하나. 두 값이 비슷할수록 출력 값이 작고, 멀수록 크다. task의 종류에 따라 사용되는 함수가 다른데 가장 대표적인 두 가지 손실함수를 먼저 알아보자.

회귀 — MSE (Mean Squared Error)

집값처럼 연속된 숫자를 맞히는 task에서는 평균제곱오차(MSE) 를 쓴다.

MSE=1ni=1n(yiy^i)2\text{MSE} = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2

각 데이터마다 정답과 예측의 차이를 제곱한 뒤 모두 평균낸다. 여기서 제곱하는 이유는 두 가지인데, 부호를 없애서 양/음 오차가 상쇄되지 않게 하고, 큰 오차에 더 무거운 벌점을 주기 위함이다. 예를 들어 오차가 2면 제곱해서 4지만, 오차가 10이면 100이 된다. 다섯 배 차이가 스물다섯 배로 벌어지니, 큰 실수일수록 손실에 훨씬 크게 반영된다.

MSE — 데이터 점과 예측선 사이의 세로 거리를 제곱해서 평균낸다
MSE — 데이터 점과 예측선 사이 거리의 제곱 평균
import numpy as np
 
y     = np.array([3.0, 5.0, 7.0])
y_hat = np.array([2.5, 5.5, 8.0])
 
mse = np.mean((y - y_hat) ** 2)
print(mse)  # 0.5

분류 — Cross-Entropy

"고양이/강아지"처럼 카테고리를 맞히는 task에서는 교차엔트로피(cross-entropy) 를 쓴다.

분류 모델 출력은 보통 카테고리별 확률 분포다 — 예: [고양이 0.7, 강아지 0.2, 새 0.1]. 정답은 정답 자리만 1, 나머지는 0인 벡터(one-hot)로 표현한다 — 예: [1, 0, 0] (정답이 고양이). 두 분포가 얼마나 다른지 재는 게 cross-entropy다.

Cross-Entropy — 정답 one-hot 분포와 예측 확률 분포의 차이
Cross-Entropy — 두 확률 분포의 차이
CE=iyilogy^i\text{CE} = -\sum_{i} y_i \log \hat{y}_i

식의 yiy_i는 정답 one-hot의 ii번째 값, y^i\hat{y}_i는 모델이 그 자리에 매긴 확률이다.

한번 직접 계산해 보면 단순함이 드러난다. 정답이 [1, 0, 0], 예측이 [0.7, 0.2, 0.1]일 때:

CE=(1log0.7+0log0.2+0log0.1)=log0.70.36\text{CE} = -(1 \cdot \log 0.7 + 0 \cdot \log 0.2 + 0 \cdot \log 0.1) = -\log 0.7 \approx 0.36

정답이 아닌 자리는 yiy_i가 0이라 항이 통째로 사라지고, 정답 자리 항만 살아남는다. 결국 cross-entropy는 모델이 정답 자리에 매긴 확률 하나에 log-\log를 씌운 값으로 줄어든다 — "모델이 정답에 얼마나 자신 있는가"만 측정하는 셈이다.

정답 확률이 1에 가까우면 손실은 0에 가깝고, 0에 가까울수록 폭발한다. 자신 있게 맞히면 보상, 자신 있게 틀리면 큰 벌점을 준다.

−log(ŷ_정답) — 정답 확률이 1에 가까우면 0, 0에 가까울수록 폭발한다
−log(ŷ_정답) 곡선
import numpy as np
 
y_hat = np.array([0.7, 0.2, 0.1])  # 모델이 매긴 확률
y     = np.array([1.0, 0.0, 0.0])  # 정답: 0번 카테고리
 
ce = -np.sum(y * np.log(y_hat))
print(ce)  # 0.3567

손실함수 선택은 단순하다

  • 출력이 연속값 → MSE 계열 (회귀)
  • 출력이 카테고리 → Cross-entropy (분류)
  • 공통 — 미분 가능해야 한다.

왜 미분이 가능해야 하는지는 다음 두 섹션(역전파, 업데이트)에서 설명한다.

역전파

손실을 줄이려면 각 파라미터를 어느 방향으로 얼마나 움직여야 손실이 줄어드는지 알아야 한다. 그 정보를 담은 값이 기울기(gradient) 다.

파라미터 ww 하나에 대한 기울기 Lw\frac{\partial L}{\partial w}는 "지금 ww를 살짝 늘리면 손실이 얼마나 늘어나는가"를 알려주는 숫자 하나다.

기울기 — 곡선 위 한 점의 접선 기울기. 부호로 방향을, 크기로 가파른 정도를 알려준다
기울기 — 한 점에서의 접선 기울기
  • 양수/음수는 방향을 담는다 — 양수면 ww를 늘릴수록 손실이 커진다는 뜻, 음수면 그 반대.
  • 크기는 손실이 그 방향으로 얼마나 가파르게 변하는지를 담는다 — 큰 값이면 한 걸음에 영향이 크고, 0에 가까우면 거의 평지.

둘을 합치면 어느 방향으로 얼마나 움직여야 할지가 정해진다.

다만 모델 안에 파라미터가 한 개가 아니라는 게 문제다. 한 step 갱신을 위해 모든 파라미터에 대해 각각의 기울기가 필요하다 — Lw1,Lw2,\frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2}, \dots.

실제 신경망은 파라미터가 수억 개고, 각 파라미터가 손실까지 도달할 때까지 여러 층을 거친다. 직접 일일이 미분해서 기울기를 구하는 건 거의 불가능하다.

그래서 나온 게 연쇄법칙(chain rule) 이라는 미분의 기본 규칙과, 그걸 활용하는 알고리즘인 역전파(backpropagation) 다.

연쇄법칙

함수가 두 단계 합성으로 되어 있다고 하자. 예를 들어 L(w)=g(w)2L(w) = g(w)^2 이고 g(w)=3w+1g(w) = 3w + 1.

ww가 바뀌면 → gg가 바뀌고 → LL이 바뀐다. 도미노 같은 사슬이다.

연쇄법칙
연쇄법칙

연쇄법칙의 핵심은 단순하다 — wwLL에 미치는 영향은 각 단계의 미분(변화율)을 모두 곱한 것과 같다.

Lw=Lggw\frac{\partial L}{\partial w} = \frac{\partial L}{\partial g} \cdot \frac{\partial g}{\partial w}

직접 계산해 보면:

  • Lg=2g\frac{\partial L}{\partial g} = 2g (L=g2L = g^2의 미분)
  • gw=3\frac{\partial g}{\partial w} = 3 (g=3w+1g = 3w+1의 미분)
  • 곱하면 2g3=6g=6(3w+1)2g \cdot 3 = 6g = 6(3w+1)

신경망도 똑같다. 단지 합성이 layer 수만큼 길어졌을 뿐. 한 가중치 ww가 손실 LL에 미친 영향은 각 layer의 미분을 끝까지 곱하면 얻을 수 있다.

왜 "뒤로" 계산하나?

곱해야 할 사슬은 정해졌다. 이걸 어느 방향으로 계산해야 효율적일까?

핵심은 여러 파라미터가 사슬의 끝부분(L 쪽)을 공유한다는 점이다. 그 공통 부분을 매번 다시 계산하느냐, 한 번만 계산하고 재활용하느냐가 갈림길.

작은 비유로 — 2×3×52 \times 3 \times 5, 3×53 \times 5, 55를 한꺼번에 다 구해야 한다고 하자. 앞에서부터 매번 처음부터 곱하면 같은 곱셈이 반복되지만, 뒤에서부터 누적해 가면 (53×5=152×15=305 \rightarrow 3 \times 5 = 15 \rightarrow 2 \times 15 = 30) 직전 결과를 그대로 재활용한다.

신경망에서도 똑같다.

  • 앞에서부터 (파라미터 → 손실) — 가중치마다 자기 자리에서 L까지 사슬을 매번 처음부터 끝까지 따라가야 한다. 가중치가 수억 개면 같은 뒷부분을 수억 번 반복
  • 뒤에서부터 (손실 → 파라미터) — L 쪽 미분을 한 번 구해 두고 한 층씩 내려가며 곱한다. 공통 부분이 자연스럽게 재활용되어 단 한 번의 backward로 모든 파라미터의 기울기가 다 나옴

이게 "역(reverse)전파"라고 부르는 이유다. 데이터는 앞으로(forward), 기울기는 뒤로(backward).

Forward vs Backward — 데이터는 앞으로, 기울기는 뒤로
Forward vs Backward

비용 차이는 엄청나다 — 파라미터마다 따로 계산하면 layer 수의 제곱에 비례하지만, 역전파는 forward 한 번 + backward 한 번이라 layer 수에 정비례한다. 이 효율성 덕분에 거대한 신경망 학습이 가능해졌다.

신경망에선 어떻게 작동하나

작은 2-layer 신경망으로 흐름을 확인하자. 순전파는 입력 xxW1W_1, ReLU, W2W_2를 차례로 거쳐 손실 LL까지 한 방향으로 흘러간다.

2-layer 신경망 순전파 — x에서 L까지 한 방향으로
2-layer 신경망 순전파

역전파는 손실 LL에서 시작해 거꾸로 한 단계씩 기울기를 흘려보낸다.

2-layer 신경망 역전파 — L에서 시작해 각 파라미터까지
2-layer 신경망 역전파

각 단계에서 하는 일은 단순하다 — 이전 단계에서 받은 기울기 × 현재 단계의 미분. 한 layer를 지날 때마다 그 layer의 가중치 기울기 LWi\frac{\partial L}{\partial W_i}하나씩 떨어져 나오고, 동시에 다음 layer로 넘길 기울기가 만들어진다. 끝까지 가면 모든 layer의 가중치 기울기가 손에 다 들려 있게 된다.

# 앞 순전파 코드의 `x, z1, a1, z2, y, W1, W2`가 있다고 가정
# 역전파: L에서 시작해 거꾸로 한 단계씩
 
dL_dz2 = -2 * (y - z2)         # 시작: ∂L/∂z₂
dL_dW2 = dL_dz2 * a1           # × a₁  → W₂의 기울기
dL_da1 = dL_dz2 * W2           # × W₂  (다음 단계로 전달)
dL_dz1 = dL_da1 * (z1 > 0)     # × ReLU′  (z₁>0이면 1, 아니면 0)
dL_dW1 = np.outer(dL_dz1, x)   # × x   → W₁의 기울기

매 줄이 "이전 단계에서 받은 기울기 × 현재 단계의 미분" 패턴이다. dL_dW2는 2층 가중치, dL_dW1은 1층 가중치의 기울기 — backward가 한 layer를 지날 때마다 그 layer의 가중치 기울기가 하나씩 떨어진 것이다. 이렇게 모은 모든 layer의 가중치 기울기를 다음 단계인 업데이트에 넘기면 한 step이 마무리된다.

업데이트

역전파가 모든 파라미터의 기울기 LW\frac{\partial L}{\partial W}를 한꺼번에 구해줬다. 이제 그 기울기를 받아 파라미터를 어떻게 움직일지 정하면 한 step이 끝난다.

경사하강법

파라미터 값이 바뀌면 손실 값도 바뀐다. 같은 데이터라도 ww가 달라지면 예측이 달라지고, 그래서 손실도 달라진다. 한 파라미터에 대해 (ww, 손실) 쌍들을 좌표평면에 찍어 이으면 U자 곡선 — 손실 곡선이 그려진다. 파라미터가 둘 이상이면 곡선이 곡면이 되지만 본질은 같다.

파라미터에 대한 손실의 곡면 — 가장 낮은 점이 우리가 가고 싶은 곳
손실 곡면 — 가장 낮은 점으로

우리가 가고 싶은 곳은 그 곡면의 최저점. 하지만 곡선 전체를 미리 알고 단번에 짚는 게 아니라, 어떤 ww(보통 랜덤)에서 시작해 지금 위치의 기울기만 보고 한 걸음씩 옮긴다. 안개 낀 산에서 하산하는 것과 똑같다 — 멀리는 안 보여도 발 밑 기울기는 알 수 있으니, 그걸 따라 한 발씩 내려가면 된다.

안개 낀 산에서 발 밑 가장 가파른 방향으로 한 걸음씩
경사하강법의 직관 — 안개 낀 산에서 한 걸음씩 하산

기울기가 양수인지 음수인지에 따라 어느 쪽이 내리막인지 알려준다.

  • 기울기 양수ww를 늘리면 손실 ↑ → ww줄여야 손실 감소
  • 기울기 음수ww를 늘리면 손실 ↓ → ww늘려야 손실 감소

두 경우 모두 기울기와 반대 방향으로 가면 손실이 줄어든다. 한 줄 식으로 정리한 게 경사하강법(gradient descent) 의 업데이트 규칙이다.

wwηLww \leftarrow w - \eta \, \frac{\partial L}{\partial w}
  • Lw\frac{\partial L}{\partial w} — 기울기 그 자체 (역전파가 구해 준 값)
  • - (마이너스) — 기울기의 반대 방향으로 가겠다는 뜻
  • η\eta (에타) — 학습률(learning rate), 한 걸음을 얼마나 크게 옮길지의 보폭
1D 손실 곡선 — 기울기 반대 방향으로 학습률만큼 이동
기울기 반대 방향으로 한 걸음

학습률 η\eta가 크면 보폭이 길고, 작으면 짧다. 너무 크면 최저점을 한 번에 건너뛰어 반대편 비탈로 튕긴다(발산). 너무 작으면 한참 걸어도 별로 안 움직인다. 보통 0.001 ~ 0.1 사이에서 고른다.

학습률 비교 — 너무 큼 / 적당함 / 너무 작음
학습률 비교

작은 예제로 직접 돌려 보기

손실이 L(w)=(w3)2L(w) = (w - 3)^2, 정답이 w=3w = 3인 단순한 상황이다. ww가 어디서 시작하든 결국 3으로 수렴해야 한다.

먼저 기울기를 손으로 구해두자. L(w)=(w3)2L(w) = (w-3)^2ww에 대해 미분하면 Lw=2(w3)\frac{\partial L}{\partial w} = 2(w - 3) 이다.

미분이 익숙하지 않다면 — 제곱한 식을 미분할 땐 지수 2가 앞으로 내려오면서 안쪽 (w3)(w-3)이 그대로 한 번 살아남는다. 앞의 2는 그래서 나온 미분 계수이고, 학습률 η\eta는 아래 코드의 lr = 0.1로 따로 등장한다.

이걸 매 step마다 계산해서 ww를 기울기 반대 방향으로 학습률만큼 옮긴다.

w  = 0.0   # 초기값
lr = 0.1   # 학습률
 
for step in range(20):
    grad = 2 * (w - 3)        # 기울기 dL/dw
    w    = w - lr * grad      # 기울기 반대 방향으로 한 걸음
    loss = (w - 3) ** 2
    print(f"step {step:2d}  w={w:.4f}  loss={loss:.4f}")

출력 (앞과 마지막):

step  0  w=0.6000  loss=5.7600
step  1  w=1.0800  loss=3.6864
step  2  w=1.4640  loss=2.3593
...
step 19  w=2.9654  loss=0.0012
L(w) = (w−3)² 위에서 w가 3으로 수렴하는 모습
1D 데모 — w가 정답 3으로 수렴

20 step 만에 거의 정답에 도착. 매번 기울기 한 번 보고 그 반대로 0.1만큼 옮긴 게 전부인데 자연스럽게 최저점으로 수렴한다. 이 예제는 파라미터가 하나라 손으로 미분이 가능했지만, 실제 신경망은 파라미터가 수억 개라 그 기울기를 한꺼번에 구하려면 앞 섹션에서 본 역전파가 필요했던 것이다.

옵티마이저

역전파가 이미 각 파라미터의 기울기 Lwi\frac{\partial L}{\partial w_i}를 다 구해 줬다. 그럼 위 1D 규칙을 w1,w2,,wnw_1, w_2, \dots, w_n 수억 개 파라미터에 어떻게 적용할까?

핵심은 단순하다 — 같은 규칙을 파라미터마다 따로따로 적용한다. 각자 자기 기울기만큼만 옮기면 끝.

w1w1ηLw1w2w2ηLw2wnwnηLwn\begin{aligned} w_1 &\leftarrow w_1 - \eta \, \frac{\partial L}{\partial w_1} \\ w_2 &\leftarrow w_2 - \eta \, \frac{\partial L}{\partial w_2} \\ &\vdots \\ w_n &\leftarrow w_n - \eta \, \frac{\partial L}{\partial w_n} \end{aligned}

수억 줄을 따로 적기는 번거로우니, 보통은 모든 파라미터를 묶어 WW로 표기하고 한 줄로 줄여 쓴다.

WWηLWW \leftarrow W - \eta \, \frac{\partial L}{\partial W}

여기서 "모든 파라미터에 동시에"는 한 step에 다 갱신된다는 뜻이지 모두가 같은 값으로 움직인다는 뜻이 아니다. 파라미터마다 자기 기울기가 다르니 옮기는 방향과 보폭도 제각각이다.

이 업데이트 단계를 수행하는 알고리즘을 통틀어 옵티마이저(optimizer) 라고 부른다. 한 번 호출하면 모델의 모든 파라미터가 위 규칙대로 한꺼번에 갱신된다.

업데이트 단계 — 옵티마이저가 모든 파라미터를 한꺼번에 갱신
업데이트 — 옵티마이저

지금까지 본 가장 단순한 형태가 SGD(Stochastic Gradient Descent) 다. 그 외에도 학습률을 자동 조절하거나 관성을 더한 Adam, RMSprop 같은 더 똑똑한 옵티마이저들이 있다 — 모두 경사하강의 변형이라 위 식을 약간씩 보정한 형태이며, 실무에선 보통 Adam을 기본값처럼 쓴다.

전부 합쳐서 — PyTorch로 모델 학습시키기

이제 네 단계를 PyTorch 라이브러리로 묶어 하나의 학습 루프로 돌려본다. 앞 섹션들에선 각 단계를 더 잘 보여주기 위해 코드를 하나하나 작성했지만, 실제론 PyTorch의 여러 유용한 메소드들(nn.Linear, nn.MSELoss, optim.SGD, loss.backward())이 그 일을 해 준다.

정답이 y=2x+1y = 2x + 1인 데이터 100개로 모델이 정답(w=2, b=1)을 찾아낼 수 있도록 학습시켜 보자.

선형회귀 setup — 정답 직선과 노이즈 데이터
선형회귀 데모 setup
import torch
import torch.nn as nn
 
# 데이터 — 정답: y = 2x + 1 (+ 약간의 noise)
x = torch.linspace(-3, 3, 100).unsqueeze(1)
y = 2 * x + 1 + torch.randn(100, 1) * 0.3
 
# 모델 · 손실 · 옵티마이저
model     = nn.Linear(1, 1)         # 학습 가능한 w, b 내장
loss_fn   = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
 
# 학습 루프 — 네 단계의 골격
for epoch in range(200):
    y_hat = model(x)                # ① 순전파
    loss  = loss_fn(y_hat, y)       # ② 손실
 
    optimizer.zero_grad()           # 이전 step의 기울기 초기화
    loss.backward()                 # ③ 역전파 (autograd가 자동 계산)
    optimizer.step()                # ④ 업데이트
 
w, b = model.weight.item(), model.bias.item()
print(f"학습 후: w={w:.2f}, b={b:.2f}")
# 학습 후: w≈2.00, b≈1.00  (노이즈에 따라 미세 변동)

핵심 메소드 설명

  • nn.Linear(in, out) — 학습 가능한 ww, bb를 내장한 선형 변환 layer
  • model.parameters() — 옵티마이저에 넘길 학습 가능 파라미터들
  • nn.MSELoss() — 평균제곱오차 (회귀용 손실 함수)
  • torch.optim.SGD(params, lr) — 경사하강 옵티마이저
  • model(x) — 순전파 실행 (내부적으로 forward(x) 호출)
  • loss.backward() — autograd가 forward 그래프를 거꾸로 타고 모든 파라미터의 기울기를 자동 계산
  • optimizer.step() — 계산된 기울기로 파라미터를 한꺼번에 업데이트
  • optimizer.zero_grad() — 이전 step의 기울기를 비움 (안 비우면 누적됨)
학습 진행 — 예측 직선이 점점 정답에 맞아 가는 모습
선형회귀 학습 진행

200 epoch 만에 정답 근처에 수렴했다.

마무리

이 글에서 본 네 단계가 모든 신경망 학습의 공통적인 구조다.

  1. 순전파 — 지금의 파라미터로 모델이 무슨 답을 내는지 계산하고
  2. 손실함수 — 그 답이 정답과 얼마나 다른지 숫자 하나로 나타내고
  3. 역전파 — 손실을 줄일 방향(기울기)을 모든 파라미터에 대해 한 번에 효율적으로 계산하고
  4. 업데이트 — 그 방향으로 모든 파라미터를 학습률만큼 한꺼번에 옮긴다 (경사하강법)
네 단계 cheatsheet
네 단계 cheatsheet

신경망이 깊어지고 task가 복잡해져도 학습 루프의 기본 뼈대는 이 네 단계다. 모델의 종류에 따라 여기서 단계가 추가되거나 변형되는 경우도 있지만, transformer, GPT, ViT 같은 모델 대부분이 이 뼈대 위에서 돌아간다.

이렇게 글로 정리하면서 각 단계를 한층 깊이 들여다볼 수 있어서 좋았다. 특히 경사하강법과 손실함수처럼 처음엔 막연하게만 알고 있던 개념도, 천천히 조사하고 직접 풀어 쓰는 과정을 거치니 비로소 머릿속에서 정리되는 것 같다.


이 글은 사실관계나 해석에 오류가 있을 수 있습니다. 잘못된 내용이나 질문이 있으면 댓글로 편하게 남겨 주세요.