[CS300 #019] 미분과 경사하강법의 수학 — 기울기를 따라 내려가기
컴퓨터공학 300 주제 시리즈의 019번째 글이다. 전체 지도는 여기.
한 줄 요약
미분은 입력을 아주 조금 바꿨을 때 출력이 얼마나 바뀌는지를 재고, 여러 변수 함수에서는 그 값들을 모은 그래디언트가 “가장 가파르게 올라가는 방향” 을 가리킨다. 경사하강법은 그 반대 방향으로 조금씩 움직여 손실을 줄이는 방법이며, 신경망 학습의 거의 전부가 이 반복이다.
왜 필요한가
머신러닝 모델의 “학습” 은 수백만~수십억 개 매개변수로 이루어진 손실 함수의 최솟값을 찾는 일이다. 방정식을 풀어 정답을 한 번에 구할 수 없으니, 현재 위치의 기울기를 보고 조금씩 내려간다. 학습률이 너무 크면 발산하고, 너무 작으면 끝나지 않는다. 손실이 NaN 이 되는 이유, 학습이 멈춘 듯 보이는 이유를 이해하려면 미분과 경사하강법의 수학이 필요하다.
머신러닝 밖에서도 쓰인다. 오토스케일러의 제어, 근 찾기(뉴턴 방법), 물리 시뮬레이션, 자동 튜닝이 모두 “기울기를 보고 조정한다” 는 같은 발상이다.
핵심 개념
도함수
함수 f 의 x 에서의 도함수는
f'(x) = lim_{h→0} (f(x+h) − f(x)) / h
다. x 에서 그래프에 접하는 직선의 기울기이고, “x 를 아주 조금 늘리면 f 가 몇 배로 변하나” 의 답이다. f’(x) > 0 이면 x 를 늘릴 때 f 가 커지고, f’(x) = 0 이면 그 점에서 평평하다(극값 후보).
| 함수 | 도함수 |
|---|---|
| c (상수) | 0 |
| xⁿ | n·xⁿ⁻¹ |
| eˣ | eˣ |
| ln x | 1/x |
| f + g | f’ + g’ |
| f·g | f’g + fg’ |
연쇄 법칙
합성 함수 y = f(g(x)) 의 도함수는
dy/dx = f'(g(x)) · g'(x) 또는 dy/dx = (dy/du)·(du/dx), u = g(x)
다. 바깥 함수의 기울기에 안쪽 함수의 기울기를 곱한다. 신경망은 층을 여러 겹 합성한 함수이므로, 출력의 손실을 맨 앞 층의 가중치로 미분하려면 연쇄 법칙을 층 수만큼 적용해야 한다. 이것을 뒤에서부터 효율적으로 계산하는 알고리즘이 역전파(backpropagation) 다.
편미분과 그래디언트
변수가 여러 개인 f(x₁, …, xₙ) 에서 다른 변수를 고정하고 xᵢ 하나로만 미분한 것이 편미분 ∂f/∂xᵢ 다. 편미분을 모은 벡터가 그래디언트다.
∇f = (∂f/∂x₁, ∂f/∂x₂, ..., ∂f/∂xₙ)
정리. 그래디언트는 f 가 가장 빠르게 증가하는 방향을 가리키며, 그 크기가 그 방향의 증가율이다.
이유. 단위 벡터 u 방향으로 조금 움직일 때 f 의 변화율(방향 도함수)은 ∇f · u = ‖∇f‖ cos θ 다(017번 글의 내적). θ = 0, 즉 u 가 ∇f 와 같은 방향일 때 가장 크다. 반대 방향 −∇f 는 가장 빠르게 감소하는 방향이다.
경사하강법
손실 함수 L(w) 를 줄이려면 그래디언트 반대 방향으로 조금 움직이는 것을 반복한다.
w ← w − η · ∇L(w)
η(에타)는 학습률이다. 이 한 줄이 경사하강법의 전부다.
학습률의 효과를 L(w) = w² 로 보자. ∇L = 2w 이므로 갱신은 w ← w − 2ηw = (1 − 2η)w 다.
| η | 1 − 2η | 결과 |
|---|---|---|
| 0.01 | 0.98 | 매우 느리게 0 으로 |
| 0.4 | 0.2 | 빠르게 0 으로 |
| 0.5 | 0 | 한 번에 0 |
| 0.9 | −0.8 | 진동하며 0 으로 |
| 1.1 | −1.2 | 진동하며 발산 |
008번 글의 점화식 a(k) = r·a(k−1) 과 같은 꼴이다. 절댓값 r 이 1 보다 작아야 수렴한다. 실제 손실 함수에서도 곡률이 큰 방향에 비해 학습률이 크면 같은 이유로 발산하고, 손실이 inf 나 NaN 이 된다.
확률적 경사하강법(SGD)과 변형들
손실이 데이터 N 개의 평균 L = (1/N)Σℓᵢ 이면 정확한 그래디언트는 N 개를 모두 봐야 한다. 확률적 경사하강법은 무작위로 뽑은 작은 묶음(미니배치)의 그래디언트로 대신한다. 그래디언트의 기댓값이 같으므로(014번 글의 선형성) 평균적으로는 올바른 방향이고, 한 걸음이 훨씬 싸다.
실무에서는 여기에 모멘텀(이전 이동 방향을 일부 이어감), Adam(변수마다 그래디언트 크기에 맞춰 보폭 조절) 같은 변형을 더한다. PyTorch 문서의 torch.optim.SGD 와 torch.optim.Adam 항목에 정확한 갱신식이 의사코드로 실려 있다(PyTorch, SGD).
볼록성과 지역 최솟값
볼록 함수(그래프 위 두 점을 이은 선분이 항상 그래프 위에 있음)는 지역 최솟값이 곧 전역 최솟값이다. 선형 회귀의 제곱 오차가 볼록이다. 신경망의 손실은 볼록이 아니어서 경사하강법이 전역 최솟값을 보장하지 못하지만, 실무에서는 충분히 좋은 지점을 찾는 경우가 많다.
자동 미분
도함수를 손으로 유도하지 않아도 된다. PyTorch 같은 프레임워크는 계산 과정을 그래프로 기록했다가 연쇄 법칙을 자동으로 적용해 그래디언트를 구한다(PyTorch, Automatic differentiation package — torch.autograd). 그래도 직접 구현한 그래디언트를 검증할 때는 수치 미분 (f(x+h) − f(x−h)) / 2h 와 비교하는 그래디언트 체크를 한다.
직접 해 보기
선형 회귀 y ≈ wx + b 를 경사하강법으로 학습한다. 손실은 평균 제곱 오차 L = (1/N)Σ(wxᵢ + b − yᵢ)² 이고, 편미분은 연쇄 법칙으로
∂L/∂w = (2/N) Σ (wxᵢ + b − yᵢ)·xᵢ
∂L/∂b = (2/N) Σ (wxᵢ + b − yᵢ)
이다. 해석적 그래디언트를 수치 미분으로 검증하고, 학습률을 바꿔 가며 결과를 본다.
import random
random.seed(0)
xs = [i / 10 for i in range(50)]
ys = [3.0 * x + 2.0 + random.gauss(0, 0.3) for x in xs] # 정답: w=3, b=2
N = len(xs)
def loss(w, b):
return sum((w * x + b - y) ** 2 for x, y in zip(xs, ys)) / N
def grad(w, b):
gw = sum(2 * (w * x + b - y) * x for x, y in zip(xs, ys)) / N
gb = sum(2 * (w * x + b - y) for x, y in zip(xs, ys)) / N
return gw, gb
# 그래디언트 체크: 중앙 차분과 비교
h, w0, b0 = 1e-5, 0.5, -1.0
num_gw = (loss(w0 + h, b0) - loss(w0 - h, b0)) / (2 * h)
print("해석:", round(grad(w0, b0)[0], 6), " 수치:", round(num_gw, 6))
def train(lr, steps=2000):
w = b = 0.0
for _ in range(steps):
gw, gb = grad(w, b)
w, b = w - lr * gw, b - lr * gb
if abs(w) > 1e6:
return "발산"
return round(w, 3), round(b, 3), round(loss(w, b), 4)
for lr in [0.001, 0.05, 0.12]:
print(lr, train(lr))
실행 결과다.
해석: -55.394464 수치: -55.394464
0.001 (3.154, 1.557, 0.1375)
0.05 (3.05, 1.89, 0.1088)
0.12 발산
해석적 그래디언트와 수치 미분이 일치하므로 미분식이 맞다. 학습률 0.05 에서는 정답(w = 3, b = 2)에 가깝게 수렴한다(잡음 때문에 정확히 3, 2 는 아니다). 0.001 은 2,000 걸음이 지나도 b 가 아직 1.56 에 머물러 가는 중이고, 0.12 는 발산한다. 이 데이터에서 x 는 0 부터 4.9 까지라 w 방향의 곡률이 커서, 학습률이 조금만 커져도 위 표의 1.1 행과 같은 일이 일어난다.
현업에서는
- 손실이 NaN 이 될 때. 가장 먼저 의심할 것은 학습률이다. 위 표처럼 곡률에 비해 보폭이 크면 값이 진동하며 커지다가 inf 를 거쳐 NaN 이 된다. 학습률을 줄이거나, 입력을 정규화해 곡률을 고르게 하거나, 그래디언트 크기를 자르는(gradient clipping) 방법을 쓴다.
- 입력 스케일링. 특성마다 크기가 다르면(예: 나이는 0~100, 연봉은 0~10⁸) 손실 함수의 등고선이 길쭉해져 경사하강법이 지그재그로 느려진다. 표준화(평균 0, 표준편차 1)가 기본 전처리인 이유다.
- 그래디언트 체크. 직접 만든 연산의 역전파를 구현했다면 수치 미분과 비교해 검증한다. 스탠퍼드 CS231n 강의 노트는 수치 그래디언트를 중앙 차분 [f(x+h) − f(x−h)] / 2h 로 구하는 편이 실무에서 더 낫다고 설명한다(CS231n, Optimization).
- 제어 루프와의 유사성. 쿠버네티스 HPA 는 현재 메트릭과 목표값의 비율로 레플리카 수를 조정한다(Kubernetes, Horizontal Pod Autoscaling). 경사하강법은 아니지만 “오차를 보고 조금씩 조정한다” 는 같은 구조이고, 너무 공격적으로 조정하면 진동한다는 교훈도 같다. HPA 에 안정화 기간(stabilization window) 설정이 있는 이유다.
확인 문제
- f(x) = (3x + 1)² 의 도함수를 연쇄 법칙으로 구하라.
- f(x, y) = x² + 3xy 의 그래디언트를 (1, 2) 에서 구하라.
- L(w) = 5w² 에 경사하강법을 쓸 때 수렴하는 학습률 η 의 범위는?
- 미니배치 그래디언트가 전체 그래디언트의 “불편 추정량” 인 이유는?
풀이
- 2(3x + 1)·3 = 6(3x + 1).
- ∇f = (2x + 3y, 3x) 이므로 (1, 2) 에서 (8, 3).
- 갱신이 w ← (1 − 10η)w 이므로 절댓값 1 − 10η 가 1 보다 작아야 한다. 0 < η < 0.2.
- 미니배치를 무작위로 뽑으면 각 샘플 그래디언트의 기댓값이 전체 평균 그래디언트와 같고, 기댓값의 선형성에 따라 그 평균도 같은 기댓값을 갖는다.
더 읽을거리 (References)
- PyTorch Documentation, torch.optim.SGD
- PyTorch Documentation, Automatic differentiation package — torch.autograd
- Stanford CS231n, Optimization: Stochastic Gradient Descent
- Ian Goodfellow, Yoshua Bengio, Aaron Courville, Deep Learning, MIT Press, 2016, 4장 Numerical Computation, 8장 Optimization