DEV Community

이종현
이종현

Posted on

수작업 미분 대신 역전파(Backpropagation) 코드로 완성하는 신경망 최적화 원리

딥러닝을 공부하다 보면 미적분이 벽처럼 느껴지는 순간이 옵니다. 편미분, 연쇄 법칙, 기울기 계산. 수식만 보면 막막하지요. 그런데 실제로 신경망이 학습하는 원리는 이렇습니다. 오차를 측정하고, 그 오차를 줄이는 방향으로 가중치를 조금씩 움직이는 것. 이 글은 그 메커니즘을 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

경사하강법 공식 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)