딥러닝을 공부하다 보면 미적분이 벽처럼 느껴지는 순간이 옵니다. 편미분, 연쇄 법칙, 기울기 계산. 수식만 보면 막막하지요. 그런데 실제로 신경망이 학습하는 원리는 이렇습니다. 오차를 측정하고, 그 오차를 줄이는 방향으로 가중치를 조금씩 움직이는 것. 이 글은 그 메커니즘을 NumPy 역전파 함수로 직접 구현하면서, 수식이 코드 안에서 어떻게 살아 움직이는지 확인합니다.
경사하강법과 미적분: 딥러닝 모델이 스스로 최적의 가중치를 찾아 업데이트하는 메커니즘
경사하강법(Gradient Descent)은 오차 함수의 기울기를 따라 가중치를 조금씩 내려가는 알고리즘입니다. 미적분의 역할은 "지금 이 지점에서 어느 방향이 내리막인가"를 수치로 알려주는 것입니다.
손실 함수(Loss Function)를 L, 가중치를 w라 할 때, 업데이트 규칙은 다음과 같습니다.
w ← w - η × (∂L/∂w)
여기서 η(에타)는 학습률(Learning Rate)로, 한 번에 얼마나 크게 내려갈지를 결정합니다. 너무 크면 발산하고, 너무 작으면 학습이 느립니다. 이 숫자 하나가 모델 성능에 생각보다 큰 영향을 미칩니다.
핵심은 편미분값, 즉 기울기(Gradient)입니다. 기울기가 양수면 w를 줄이고, 음수면 w를 늘려서 손실이 작아지는 방향으로 이동합니다. 수식을 외울 필요는 없습니다. "내리막 방향으로 한 걸음"이라는 감각이 먼저입니다.
편미분과 연쇄 법칙(Chain Rule): 여러 신경망 계층을 거슬러 오차를 전달하는 흐름
신경망에는 계층(Layer)이 여러 개 있습니다. 출력층에서 계산한 오차를 입력층 방향으로 역으로 전달해야 각 가중치의 기울기를 구할 수 있습니다. 이 과정이 역전파(Backpropagation)입니다.
여기서 연쇄 법칙(Chain Rule)이 등장합니다. 합성 함수의 미분을 분해하는 규칙으로, 수식으로 쓰면 이렇습니다.
∂L/∂w = (∂L/∂y) × (∂y/∂w)
각 항의 의미를 풀어보면:
-
∂L/∂y: 출력값 y가 손실 L에 얼마나 영향을 주는지 (뒤에서 넘어온 기울기) -
∂y/∂w: 가중치 w가 출력값 y에 얼마나 영향을 주는지 (현재 계층의 기울기)
이 두 값을 곱하면 "이 가중치가 최종 오차에 얼마나 책임이 있는가"가 나옵니다. 계층이 10개라도 이 곱셈을 순서대로 거슬러 올라가면 됩니다. 코드 관점에서는 각 계층이 순전파(Forward Pass) 때 계산한 중간 값을 저장해 두고, 역전파 때 꺼내 쓰는 구조입니다.
활성화 함수로 시그모이드(Sigmoid)를 쓴다면, 그 미분값은 σ(x) × (1 - σ(x))입니다. 이 값이 1보다 작기 때문에 계층을 많이 쌓을수록 기울기가 점점 작아지는 기울기 소실(Vanishing Gradient) 문제가 생깁니다. ReLU 같은 함수가 나온 이유 중 하나입니다.
어떤 손실 함수를 선택해야 역전파가 안정적으로 작동할까?
역전파의 안정성은 손실 함수 선택과 밀접합니다. 잘못 선택하면 기울기가 제대로 전달되지 않아 학습이 겉돌 수 있습니다.
| 상황 | 권장 손실 함수 | 출력층 활성화 함수 | 기울기 특성 |
|---|---|---|---|
| 이진 분류 | Binary Cross-Entropy | Sigmoid | 안정적, 포화 문제 적음 |
| 다중 분류 | Categorical Cross-Entropy | Softmax | 확률 합 = 1 보장 |
| 회귀 | MSE (Mean Squared Error) | 선형(없음) | 이상치에 민감 |
| 회귀 (이상치 많음) | MAE 또는 Huber Loss | 선형(없음) | 기울기 폭발 억제 |
표에서 보듯, 분류 문제에 MSE를 쓰면 기울기 계산이 비효율적으로 됩니다. 손실 함수는 편의 선택이 아니라, 역전파 흐름 자체를 설계하는 결정입니다.
수식 너머의 실전 코드: NumPy로 오차 역전파 함수를 빌드하는 연습
수식을 코드로 바꾸는 연습이 이해를 굳힙니다. 아래는 1개의 은닉층을 가진 신경망을 NumPy만으로 구현하는 흐름입니다. 실제로 돌아가는 코드 구조를 단계별로 보겠습니다.
1단계: 순전파(Forward Pass) 정의
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def forward(X, W1, b1, W2, b2):
Z1 = X @ W1 + b1 # 은닉층 선형 변환
A1 = sigmoid(Z1) # 활성화
Z2 = A1 @ W2 + b2 # 출력층 선형 변환
A2 = sigmoid(Z2) # 최종 출력 (이진 분류)
cache = (X, Z1, A1, Z2, A2)
return A2, cache
cache에 중간 값을 저장하는 것이 핵심입니다. 역전파 때 이 값들을 꺼내 기울기 계산에 씁니다.
2단계: 손실 함수 계산
def compute_loss(A2, Y):
m = Y.shape[0]
loss = -np.mean(Y * np.log(A2 + 1e-8) + (1 - Y) * np.log(1 - A2 + 1e-8))
return loss
1e-8을 더하는 이유는 log(0) 오류를 막기 위해서입니다. 작은 디테일이지만 없으면 런타임 에러가 납니다.
3단계: 역전파(Backward Pass) 구현
def backward(cache, W2, Y, learning_rate=0.01):
X, Z1, A1, Z2, A2 = cache
m = Y.shape[0]
# 출력층 기울기
dZ2 = A2 - Y
dW2 = (A1.T @ dZ2) / m
db2 = np.mean(dZ2, axis=0)
# 연쇄 법칙: 은닉층으로 기울기 전달
dA1 = dZ2 @ W2.T
dZ1 = dA1 * A1 * (1 - A1) # 시그모이드 미분
dW1 = (X.T @ dZ1) / m
db1 = np.mean(dZ1, axis=0)
return dW1, db1, dW2, db2
dA1 * A1 * (1 - A1) 부분이 연쇄 법칙의 실제 코드 표현입니다. 뒤에서 온 기울기(dA1)와 현재 계층의 미분(A1 * (1 - A1))을 곱합니다. 수식이 코드 한 줄로 압축됩니다.
4단계: 가중치 업데이트
def update(W1, b1, W2, b2, dW1, db1, dW2, db2, lr=0.01):
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
return W1, b1, W2, b2
경사하강법 공식 w ← w - η × gradient가 그대로입니다. 복잡해 보이던 미적분이 빼기와 곱하기로 끝납니다.
주의해야 할 점이 있습니다. 가중치 초기화를 0으로 하면 모든 뉴런이 똑같이 학습해서 은닉층이 의미를 잃습니다. np.random.randn() 같은 난수 초기화가 필요한 이유입니다. 처음에 이 부분에서 막히는 경우가 많습니다.
모두의AI 딥러닝 실천 과정: 코드 플레이그라운드로 역전파 개념을 체화하는 방법
개념을 이해하고 코드를 짜는 것과, 실제로 돌려보면서 "이 값이 왜 이렇게 나오지?"를 경험하는 것은 다릅니다. 두 번째 경험이 진짜 학습입니다.
모두의AI 딥러닝 실천 과정은 이 차이를 메우는 데 초점을 맞춥니다. 개념 설명 다음에 바로 코드 플레이그라운드가 열리고, 학습률을 바꿔보거나 계층 수를 늘려보면서 손실이 어떻게 달라지는지 직접 확인합니다. 수식을 외우는 게 아니라, 파라미터를 건드리면서 신경망이 반응하는 방식을 몸으로 익히는 구조입니다.
주니어 개발자라면 특히 역전파 함수에서 shape 오류가 자주 납니다. 행렬 곱의 차원이 맞지 않아 생기는 오류인데, 이 과정에서는 각 단계마다 텐서의 차원을 확인하는 습관을 같이 잡아줍니다. 코드가 돌아가는 것과 올바르게 돌아가는 것의 차이를 구분하는 눈이 생기는 지점입니다.
자주 묻는 질문
미적분을 깊이 공부하지 않아도 역전파를 구현할 수 있을까요?
연쇄 법칙의 직관, 즉 "앞 계층의 기울기를 뒤 계층에서 받아서 현재 계층 미분과 곱한다"는 흐름을 이해하면 충분합니다. 수식 유도를 처음부터 할 줄 몰라도 NumPy 코드로 구현하고 검증하는 데는 지장이 없습니다.
학습률(Learning Rate)을 어떻게 정해야 할까요?
정해진 공식은 없습니다. 보통 0.001에서 시작해서 손실이 줄지 않으면 줄이고, 발산하면 더 줄입니다. Adam 같은 적응형 옵티마이저는 이 조정을 자동으로 해줘서, 초반에는 Adam을 쓰면서 감각을 익히는 것이 실용적입니다.
기울기 소실(Vanishing Gradient) 문제는 어떻게 피할 수 있을까요?
시그모이드 대신 ReLU 활성화 함수를 쓰고, 배치 정규화(Batch Normalization)를 적용하는 것이 현재 가장 일반적인 방법입니다. 깊은 네트워크일수록 이 선택이 학습 안정성에 직접 영향을 미칩니다.
NumPy 역전파 코드와 PyTorch autograd의 차이는 무엇인가요?
PyTorch의 autograd는 연산 그래프를 자동으로 추적해서 역전파를 대신 계산합니다. NumPy로 직접 구현하면 각 계층의 기울기 흐름을 눈으로 확인할 수 있어서, 원리를 이해하는 데 훨씬 효과적입니다. 원리를 먼저, 편의 도구는 그 다음입니다.
역전파 구현 연습에 가장 좋은 데이터셋은 무엇인가요?
XOR 문제나 사이킷런(scikit-learn)의 make_moons 데이터가 적합합니다. 데이터가 작고, 비선형 패턴이 있어서 은닉층의 효과를 눈으로 확인하기 쉽습니다. MNIST는 그 다음 단계로 넘어갈 때 쓰면 됩니다.
딥러닝의 미적분 원리는 수식 암기에 있지 않습니다. 경사하강법이 오차를 줄이는 방향을 찾고, 연쇄 법칙이 그 신호를 계층마다 거슬러 전달하며, 역전파 코드가 그 흐름을 실제로 실행합니다. 이 세 단계를 NumPy로 한 번 직접 써보면 블랙박스처럼 느껴지던 신경망이 다르게 보입니다. 모두의AI 실전 예제 챕터에서 직접 파이썬 역전파 코드를 돌려보며 신경망 최적화를 검증해 보세요.
더 보기: mdooai.com
Top comments (0)