본문 바로가기

딥러닝/딥러닝 수학 기초

미적분 핵심 (미분, 편미분, 연쇄법칙)

들어가며

딥러닝을 공부하면서 미적분이 왜 필요한지 처음엔 이해가 안 됐다. 결국 딥러닝의 학습 과정은 "손실값을 줄이는 방향으로 가중치를 조금씩 조정하는 것"인데, 이 "방향"을 찾는 도구가 미분이다. 웹 개발에서 UI 최적화를 위해 성능을 측정하고 병목을 찾는 것처럼, 딥러닝에서는 미분으로 어떤 가중치를 얼마나 바꿔야 하는지 찾는다.


미분이란?

함수에서 입력값이 아주 조금 변할 때 출력값이 얼마나 변하는지를 나타내는 값이다. 즉 변화율이다.

f'(x) = lim(h→0) [f(x+h) - f(x)] / h

직관적으로 이해하면 이렇다.

f(x) = x²

x = 3일 때 f'(x) = 6
→ x가 1 증가하면 f(x)는 약 6 증가한다
→ x가 0.01 증가하면 f(x)는 약 0.06 증가한다

그래프에서의 의미

미분값은 그래프에서 해당 점의 기울기(slope)다.

기울기 > 0 : 오른쪽으로 갈수록 값이 증가
기울기 < 0 : 오른쪽으로 갈수록 값이 감소
기울기 = 0 : 극값 (최솟값 또는 최댓값)

딥러닝에서 목표는 손실 함수의 최솟값을 찾는 것이다. 수천만 개의 가중치를 가진 신경망에서 기울기가 정확히 0인 지점을 찾는 건 불가능하다. 그래서 경사하강법(Gradient Descent)으로 조금씩 최솟값 방향으로 이동한다.


기본 미분 공식

상수:     f(x) = c            →  f'(x) = 0
거듭제곱:  f(x) = xⁿ           →  f'(x) = nxⁿ⁻¹
지수:     f(x) = eˣ            →  f'(x) = eˣ
로그:     f(x) = ln(x)         →  f'(x) = 1/x
합:       f(x) = g(x) + h(x)  →  f'(x) = g'(x) + h'(x)
import numpy as np

# 수치 미분으로 검증
def numerical_diff(f, x, h=1e-5):
    return (f(x + h) - f(x - h)) / (2 * h)

# f(x) = x²
f = lambda x: x ** 2
print(numerical_diff(f, 3.0))   # ≈ 6.0  (이론값: 2*3 = 6)
print(numerical_diff(f, 5.0))   # ≈ 10.0 (이론값: 2*5 = 10)

# f(x) = x³
f2 = lambda x: x ** 3
print(numerical_diff(f2, 2.0))  # ≈ 12.0 (이론값: 3*2² = 12)

# f(x) = e^x
f3 = lambda x: np.exp(x)
print(numerical_diff(f3, 1.0))  # ≈ 2.718 (이론값: e¹ ≈ 2.718)

딥러닝에서 자주 쓰이는 함수의 미분

ReLU 함수

딥러닝에서 가장 많이 쓰이는 활성화 함수다.

f(x) = max(0, x)

f'(x) = 1  (x > 0)
f'(x) = 0  (x ≤ 0)
def relu(x):
    return np.maximum(0, x)

def relu_derivative(x):
    return (x > 0).astype(float)

x = np.array([-2, -1, 0, 1, 2])
print(relu(x))             # [0, 0, 0, 1, 2]
print(relu_derivative(x))  # [0, 0, 0, 1, 1]

직관적으로: x가 양수일 때는 그대로 통과(기울기 1), 음수일 때는 0으로 막는다(기울기 0).

Sigmoid 함수

이진 분류의 출력층, LSTM 게이트 등에서 사용된다.

f(x) = 1 / (1 + e⁻ˣ)

f'(x) = f(x) * (1 - f(x))
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    s = sigmoid(x)
    return s * (1 - s)

x = np.array([-2, -1, 0, 1, 2])
print(sigmoid(x))
# [0.119, 0.269, 0.5, 0.731, 0.881]

print(sigmoid_derivative(x))
# [0.105, 0.197, 0.25, 0.197, 0.105]
# → x=0일 때 기울기 최대 (0.25), 양 끝으로 갈수록 기울기 소멸

기울기 소실 문제: Sigmoid의 기울기는 최대 0.25다. 레이어가 깊어질수록 역전파 시 기울기가 점점 작아져서 0에 수렴한다. 이것이 Vanishing Gradient 문제다. 이 때문에 깊은 신경망에서는 ReLU를 주로 사용한다.

Softmax 함수

다중 분류의 출력층에서 사용된다. 출력값의 합이 1이 되어 확률로 해석할 수 있다.

def softmax(x):
    # 수치 안정성을 위해 최댓값을 빼줌
    exp_x = np.exp(x - np.max(x))
    return exp_x / exp_x.sum()

logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)
print(probs)        # [0.659, 0.242, 0.099]
print(probs.sum())  # 1.0

편미분 (Partial Derivative)

변수가 여러 개일 때 하나의 변수에 대해서만 미분하는 것이다. 나머지 변수는 상수로 취급한다.

f(x, y) = x² + 2xy + y²

∂f/∂x = 2x + 2y   (y는 상수 취급)
∂f/∂y = 2x + 2y   (x는 상수 취급)

딥러닝에서 신경망은 수백만 개의 가중치를 가진다. 손실 함수 L을 각 가중치에 대해 편미분하면 각 가중치가 손실에 얼마나 영향을 미치는지 알 수 있다.

def partial_diff(f, params, idx, h=1e-5):
    params_plus = params.copy()
    params_minus = params.copy()
    params_plus[idx] += h
    params_minus[idx] -= h
    return (f(params_plus) - f(params_minus)) / (2 * h)

# f(x, y) = x² + 2xy + y²
def f(params):
    x, y = params
    return x**2 + 2*x*y + y**2

params = np.array([1.0, 2.0])
df_dx = partial_diff(f, params, 0)
df_dy = partial_diff(f, params, 1)

print(f"∂f/∂x = {df_dx:.4f}")  # ≈ 6.0 (이론값: 2*1 + 2*2 = 6)
print(f"∂f/∂y = {df_dy:.4f}")  # ≈ 6.0 (이론값: 2*1 + 2*2 = 6)

그래디언트 (Gradient)

모든 변수에 대한 편미분을 벡터로 모아 놓은 것이다. 손실 함수가 가장 빠르게 증가하는 방향을 가리킨다.

∇f = [∂f/∂w₁, ∂f/∂w₂, ..., ∂f/∂wₙ]
def gradient(f, params, h=1e-5):
    grad = np.zeros_like(params)
    for i in range(len(params)):
        grad[i] = partial_diff(f, params, i, h)
    return grad

# 손실 함수: L(w1, w2) = w1² + w2²
def loss(params):
    return params[0]**2 + params[1]**2

params = np.array([3.0, 4.0])
grad = gradient(loss, params)
print(f"그래디언트: {grad}")  # [6. 8.] (이론값: [2*3, 2*4])

그래디언트는 손실이 가장 빠르게 증가하는 방향을 가리킨다. 손실을 줄이려면 그래디언트의 반대 방향으로 이동하면 된다. 이것이 경사하강법의 핵심이다.

# 경사하강법으로 최솟값 찾기
params = np.array([3.0, 4.0])
lr = 0.1

print(f"초기 params: {params}, loss: {loss(params):.4f}")

for step in range(20):
    grad = gradient(loss, params)
    params = params - lr * grad  # 그래디언트 반대 방향으로 이동

    if (step + 1) % 5 == 0:
        print(f"step {step+1}: params={params.round(4)}, loss={loss(params):.6f}")

# step  5: loss=5.849...
# step 10: loss=0.583...
# step 15: loss=0.092...
# step 20: loss=0.014... → 점점 0에 가까워짐

연쇄 법칙 (Chain Rule)

딥러닝 역전파의 핵심 원리다. 합성 함수를 미분할 때 사용한다.

y = f(g(x)) 일 때,
dy/dx = dy/dg * dg/dx

직관적으로: "최종 출력이 중간 변수에 얼마나 의존하는가" × "중간 변수가 입력에 얼마나 의존하는가"

간단한 예시

x → [g(x) = 2x] → z → [f(z) = z²] → y

y = f(g(x)) = (2x)² = 4x²

연쇄 법칙으로 계산:
dy/dz = 2z    (f를 z로 미분)
dz/dx = 2     (g를 x로 미분)
dy/dx = dy/dz * dz/dx = 2z * 2 = 4(2x) = 8x

검증: y = 4x² → dy/dx = 8x ✓
x = 3.0

# 순전파
z = 2 * x
y = z ** 2

# 역전파 (연쇄 법칙)
dy_dz = 2 * z
dz_dx = 2
dy_dx = dy_dz * dz_dx

print(f"순전파: z={z}, y={y}")
print(f"역전파: dy/dz={dy_dz}, dz/dx={dz_dx}, dy/dx={dy_dx}")
# dy/dx = 24 (이론값: 8*3 = 24) ✓

신경망에서 연쇄 법칙

신경망은 여러 레이어가 합성된 함수다.

x → [레이어1] → h1 → [레이어2] → h2 → [손실함수] → L

역전파:
∂L/∂x = ∂L/∂h2 * ∂h2/∂h1 * ∂h1/∂x
np.random.seed(42)

# 순전파
x = np.array([1.0, 2.0, 3.0])
W1 = np.random.randn(3, 4) * 0.1
b1 = np.zeros(4)
z1 = x @ W1 + b1
h1 = np.maximum(0, z1)    # ReLU

W2 = np.random.randn(4, 1) * 0.1
b2 = np.zeros(1)
z2 = h1 @ W2 + b2
y_pred = z2.squeeze()

y_true = 1.0
loss_val = (y_pred - y_true) ** 2

# 역전파 (연쇄 법칙)
dL_dy = 2 * (y_pred - y_true)
dL_dz2 = dL_dy
dL_dW2 = h1.reshape(-1, 1) * dL_dz2
dL_dh1 = (W2 * dL_dz2).squeeze()
dL_dz1 = dL_dh1 * (z1 > 0)          # ReLU 미분
dL_dW1 = np.outer(x, dL_dz1)

print(f"W1 그래디언트 shape: {dL_dW1.shape}")  # (3, 4)
print(f"W2 그래디언트 shape: {dL_dW2.shape}")  # (4, 1)

PyTorch의 자동 미분 (autograd)

실제 딥러닝에서는 연쇄 법칙을 직접 계산하지 않는다. PyTorch가 계산 그래프를 자동으로 구성하고 역전파를 수행해준다.

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
W = torch.tensor([[0.1, 0.2, 0.3, 0.4],
                  [0.5, 0.6, 0.7, 0.8],
                  [0.9, 1.0, 1.1, 1.2]])

# 순전파
z = x @ W
h = torch.relu(z)
loss = h.sum()

# 역전파 (자동으로 연쇄 법칙 적용)
loss.backward()

print(f"x의 그래디언트: {x.grad}")
# PyTorch가 자동으로 ∂loss/∂x를 계산

딥러닝 프레임워크를 쓰면 연쇄 법칙을 직접 계산할 필요가 없다. 하지만 내부에서 어떤 일이 일어나는지 이해하면 버그를 디버깅하거나 커스텀 레이어를 만들 때 훨씬 수월하다.


정리

개념 설명 딥러닝 활용
미분 함수의 변화율 (기울기) 가중치 업데이트 방향
편미분 다변수 함수에서 하나의 변수로 미분 각 가중치의 영향도 계산
그래디언트 모든 편미분을 모은 벡터 경사하강법의 이동 방향
연쇄 법칙 합성 함수의 미분 역전파(Backpropagation)
ReLU 미분 x>0이면 1, x≤0이면 0 역전파 시 기울기 전달
Sigmoid 미분 s(x) * (1 - s(x)) 기울기 소실 문제와 연관
수치 미분 근사적으로 미분값 계산 그래디언트 검증