콘텐츠로 이동

6차시: 역전파 - 신경망은 어떻게 스스로 배우는가

⏰ 80분 · 연쇄법칙 · 경사하강법 · 역전파 · NumPy 구현 · 난이도 ●●●●○

학습목표: 연쇄법칙으로 역전파의 원리를 설명하고, NumPy로 2층 신경망의 순전파-역전파를 구현한다.

오늘의 질문: “시험에서 틀린 문제를 보고 ‘아, 이 부분을 다시 공부해야겠다’고 생각한 적이 있습니까? 신경망도 똑같은 방식으로 배운다면, 그 ‘다시 공부해야겠다’를 수학적으로 어떻게 표현할 수 있을까요?”


오차를 거꾸로 흘려보내 각 가중치의 ‘책임’을 계산하는 수학적 원리

너무 크면 발산, 너무 작으면 정체 — 적절한 보폭은 어떻게 정할까?

XOR 문제를 푸는 신경망을 바닥부터 직접 구현

손실이 줄어드는 과정을 그래프로 확인하고 해석


1단계: 도입 — 산에서 내려오는 문제 (10분)

섹션 제목: “1단계: 도입 — 산에서 내려오는 문제 (10분)”

안개 낀 산에서 발밑 경사만 보고 골짜기로 내려가는 비유로 경사하강법을 직관적으로 이해합니다. 학습률이라는 ‘보폭’의 개념을 체험적으로 도입합니다.

2단계: 연쇄법칙과 역전파 원리 (20분)

섹션 제목: “2단계: 연쇄법칙과 역전파 원리 (20분)”

단일 뉴런의 미분부터 시작해 2층 신경망까지 연쇄법칙을 적용하는 과정을 수식으로 따라갑니다. 각 가중치가 최종 오차에 기여한 ‘책임’을 계산하는 원리를 이해합니다.

3단계: NumPy로 XOR 신경망 구현 (35분)

섹션 제목: “3단계: NumPy로 XOR 신경망 구현 (35분)”

순전파 v1 → 역전파 추가 v2 → 학습 루프 완성 v3 순서로 2층 신경망을 점진적으로 만들어 봅니다. XOR을 학습하는 과정을 에포크별 손실로 확인합니다.

4단계: 학습률 실험과 성찰 (15분)

섹션 제목: “4단계: 학습률 실험과 성찰 (15분)”

학습률을 0.001, 0.1, 10으로 바꿔가며 발산·정체·수렴의 차이를 관찰합니다. 형성 평가로 마무리합니다.


1. 손실 함수 — 신경망이 얼마나 틀렸는가

섹션 제목: “1. 손실 함수 — 신경망이 얼마나 틀렸는가”

시험 점수가 60점이라고 가정해 보겠습니다. 만점까지 40점이 부족한 상태입니다. 이 ‘부족한 정도’를 숫자 하나로 표현한 것이 손실(loss)입니다. 신경망에서도 똑같습니다. 신경망이 예측한 값과 실제 정답의 차이를 하나의 숫자로 요약합니다.

가장 많이 쓰는 손실 함수는 평균제곱오차(MSE)입니다. 예측값을 $\hat{y}$, 실제값을 $y$라고 할 때:

$ L = \dfrac{1}{2}(\hat{y} - y)^2 $

여기서 $L$은 손실(Loss), $\hat{y}$은 신경망의 예측, $y$는 정답입니다. 제곱을 하는 이유는 양수·음수 오차가 상쇄되지 않게 하기 위함입니다. 앞에 $\dfrac{1}{2}$을 붙이는 이유는 미분할 때 2가 내려와서 깔끔해지기 때문입니다(수식 편의).

학습의 목표는 단 하나입니다: 이 $L$을 최대한 작게 만드는 가중치를 찾는 것.


2. 경사하강법 — 산에서 내려오는 방법

섹션 제목: “2. 경사하강법 — 산에서 내려오는 방법”

짙은 안개가 낀 산 정상에 서 있다고 가정하겠습니다. 골짜기로 내려가야 하는데 시야가 1미터뿐입니다. 이때 쓸 수 있는 전략은 하나입니다. 발밑에서 가장 가파르게 내려가는 방향을 찾아 한 걸음씩 내려가는 것입니다. 그 걸음을 수천 번 반복하면 결국 골짜기 근처에 도달합니다.

이것이 바로 경사하강법(Gradient Descent)입니다. 손실 함수 $L$을 ‘산의 높이’로 보고, 가중치 $w$를 ‘위치’로 봅니다. 각 지점에서 기울기(gradient) $\dfrac{\partial L}{\partial w}$을 계산해, 기울기의 반대 방향으로 조금씩 움직입니다.

$ w_{\text{new}} = w_{\text{old}} - \eta \cdot \dfrac{\partial L}{\partial w} \tag{1} $

여기서 $\eta$(에타)는 학습률(learning rate), 즉 한 걸음의 보폭입니다. $\dfrac{\partial L}{\partial w}$은 가중치 $w$를 아주 조금 바꿨을 때 손실 $L$이 얼마나 변하는가를 나타냅니다.

학습률 $\eta$결과비유
너무 작음 (0.0001)거의 제자리, 학습이 안 됨개미걸음
적절 (0.01-0.1)꾸준히 골짜기로 수렴등산객
너무 큼 (10)골짜기를 건너뛰어 발산산을 뛰어넘는 거인

문제 하나를 풀어 보겠습니다. 빵 가격이 오르면 샌드위치 가격이 오르고, 샌드위치가 오르면 제 점심값이 오릅니다. 그렇다면 “빵 가격이 100원 오르면 내 점심값은 얼마나 오를까?”

답은 간단합니다. (빵이 오를 때 샌드위치 오르는 비율) × (샌드위치가 오를 때 점심값 오르는 비율)을 곱하면 됩니다. 이것이 연쇄법칙(Chain Rule)입니다.

수식으로 표현하면, $z$가 $y$의 함수이고 $y$가 $x$의 함수일 때:

$ \dfrac{\partial z}{\partial x} = \dfrac{\partial z}{\partial y} \cdot \dfrac{\partial y}{\partial x} \tag{2} $

역전파(Backpropagation)는 연쇄법칙을 신경망에 적용한 것입니다. 신경망은 입력 → 은닉층 → 출력층 → 손실 순서로 계산이 흘러갑니다(순전파). 반대로, 손실에서 출발해 각 가중치가 손실에 미친 영향을 역순으로 계산해 내려옵니다(역전파).

flowchart LR
  X[입력 x] --> H["은닉층<br/>h = σ(W1·x + b1)"]
  H --> Y["출력<br/>ŷ = σ(W2·h + b2)"]
  Y --> L[손실 L]
  L -.역전파 기울기.-> Y
  Y -.∂L/∂W2.-> H
  H -.∂L/∂W1.-> X

순전파는 실선(→), 역전파는 점선(-.->)으로 표시된 흐름입니다. 손실에서 출발한 기울기가 $W_2$, 그다음 $W_1$ 순서로 전달됩니다.

왜 이것이 중요한가? 신경망 가중치가 100만 개라면, 각 가중치마다 수치 미분으로 기울기를 계산하면 100만 번의 순전파가 필요합니다. 역전파는 단 한 번의 순전파와 한 번의 역전파로 모든 가중치의 기울기를 동시에 얻습니다. 1986년 힌튼·루멜하트·윌리엄스의 이 아이디어가 딥러닝을 가능하게 만든 핵심입니다.


이제 실제로 XOR을 풀 2층 신경망에 연쇄법칙을 적용해 보겠습니다. 구조는 다음과 같습니다.

기호의미크기
$\mathbf{x}$입력2차원
$W_1, b_1$은닉층 가중치, 편향2×4, 4
$\mathbf{h}$은닉층 출력4차원
$W_2, b_2$출력층 가중치, 편향4×1, 1
$\hat{y}$최종 예측1차원
$\sigma$시그모이드 활성화

순전파는 아래와 같이 흘러갑니다.

$ \begin{aligned} \mathbf{z_1} &= W_1 \mathbf{x} + b_1 \ \mathbf{h} &= \sigma(\mathbf{z_1}) \ z_2 &= W_2 \mathbf{h} + b_2 \ \hat{y} &= \sigma(z_2) \ L &= \dfrac{1}{2}(\hat{y} - y)^2 \end{aligned} $

역전파는 손실에서 거꾸로 내려오며 연쇄법칙을 적용합니다. 시그모이드의 미분은 $\sigma’(z) = \sigma(z)(1 - \sigma(z))$라는 편리한 성질이 있습니다.

$ \begin{aligned} \delta_2 &= (\hat{y} - y) \cdot \hat{y}(1 - \hat{y}) \ \dfrac{\partial L}{\partial W_2} &= \delta_2 \cdot \mathbf{h}^T \ \delta_1 &= (W_2^T \delta_2) \odot \mathbf{h}(1 - \mathbf{h}) \ \dfrac{\partial L}{\partial W_1} &= \delta_1 \cdot \mathbf{x}^T \end{aligned} $

$\delta$는 ‘오차 신호’로, 각 층이 얼마만큼 ‘책임’이 있는지를 나타냅니다. $\odot$은 원소별 곱입니다. 이 네 줄이 역전파의 전부입니다.


🔧 실습 활동: NumPy로 XOR 신경망 만들기

섹션 제목: “🔧 실습 활동: NumPy로 XOR 신경망 만들기”
  • Python 3.10+, NumPy, Matplotlib
  • 설치: pip install numpy matplotlib

XOR(배타적 논리합)은 입력 두 개가 다를 때만 1을 출력합니다.

x1x2XOR
000
011
101
110

이 문제는 단층 퍼셉트론으로 절대 풀 수 없습니다(직선 하나로 1과 0을 나눌 수 없음). 은닉층 + 역전파가 왜 필요한지 보여주는 역사적 예제입니다.


🔹 v1: 순전파만 구현 (기본 뼈대)

섹션 제목: “🔹 v1: 순전파만 구현 (기본 뼈대)”

먼저 가중치를 랜덤으로 초기화하고 순전파만 해 보겠습니다.

import numpy as np
# XOR 데이터
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) # 입력 4개
y = np.array([[0], [1], [1], [0]]) # 정답
# 시그모이드 활성화 함수 (왜: 출력을 0-1로 제한, 미분이 간단)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 가중치 초기화 (왜: 너무 크면 포화, 너무 작으면 학습 X)
np.random.seed(42)
W1 = np.random.randn(2, 4) * 0.5 # 입력 2 -> 은닉 4
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5 # 은닉 4 -> 출력 1
b2 = np.zeros((1, 1))
# 순전파
z1 = X @ W1 + b1 # 선형 조합
h = sigmoid(z1) # 은닉층 출력
z2 = h @ W2 + b2
y_hat = sigmoid(z2) # 최종 예측
print("예측값:\n", y_hat)
print("정답:\n", y)
예측값:
[[0.52 ]
[0.51 ]
[0.54 ]
[0.53 ]]
정답:
[[0]
[1]
[1]
[0]]

학습 전이라 모든 출력이 0.5 근처입니다. 아직 아무것도 배우지 않았습니다.


🔹 v2: 역전파 추가 — 한 번의 업데이트

섹션 제목: “🔹 v2: 역전파 추가 — 한 번의 업데이트”

이제 오차를 역전파해 가중치를 한 번만 업데이트해 보겠습니다.

import numpy as np
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def sigmoid_deriv(a):
# 이미 sigmoid를 통과한 값 a를 받음 (왜: 계산 효율)
return a * (1 - a)
np.random.seed(42)
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))
lr = 0.1 # 학습률
# --- 순전파 ---
z1 = X @ W1 + b1
h = sigmoid(z1)
z2 = h @ W2 + b2
y_hat = sigmoid(z2)
# --- 손실 ---
loss = 0.5 * np.mean((y_hat - y) ** 2)
print(f"업데이트 전 손실: {loss:.4f}")
# --- 역전파 (연쇄법칙) ---
delta2 = (y_hat - y) * sigmoid_deriv(y_hat) # <- 여기가 출력층 오차 신호
dW2 = h.T @ delta2 # <- ∂L/∂W2
db2 = np.sum(delta2, axis=0, keepdims=True)
delta1 = (delta2 @ W2.T) * sigmoid_deriv(h) # <- 은닉층 오차 신호 (연쇄법칙)
dW1 = X.T @ delta1 # <- ∂L/∂W1
db1 = np.sum(delta1, axis=0, keepdims=True)
# --- 가중치 업데이트 (경사하강) ---
W2 -= lr * dW2
b2 -= lr * db2
W1 -= lr * dW1
b1 -= lr * db1
# --- 업데이트 후 손실 확인 ---
h_new = sigmoid(X @ W1 + b1)
y_hat_new = sigmoid(h_new @ W2 + b2)
loss_new = 0.5 * np.mean((y_hat_new - y) ** 2)
print(f"업데이트 후 손실: {loss_new:.4f}")
업데이트 전 손실: 0.1283
업데이트 후 손실: 0.1279

한 번의 업데이트로 손실이 아주 조금 줄었습니다. 위 코드의 25번째 줄 delta2 = (y_hat - y) * sigmoid_deriv(y_hat)이 바로 출력층의 오차 신호($\delta_2$)이고, 28번째 줄 delta1 = (delta2 @ W2.T) * sigmoid_deriv(h)연쇄법칙으로 전달된 은닉층 오차 신호($\delta_1$)입니다.


🔹 v3: 학습 루프 완성 — 5000번 반복

섹션 제목: “🔹 v3: 학습 루프 완성 — 5000번 반복”

한 번으로는 부족합니다. 수천 번 반복해야 합니다.

import numpy as np
import matplotlib.pyplot as plt
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def sigmoid_deriv(a):
return a * (1 - a)
np.random.seed(42)
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))
lr = 0.5
epochs = 5000
losses = [] # 학습 곡선용
for epoch in range(epochs):
# 순전파
h = sigmoid(X @ W1 + b1)
y_hat = sigmoid(h @ W2 + b2)
# 손실 기록
loss = 0.5 * np.mean((y_hat - y) ** 2)
losses.append(loss)
# 역전파
delta2 = (y_hat - y) * sigmoid_deriv(y_hat)
delta1 = (delta2 @ W2.T) * sigmoid_deriv(h)
# 업데이트 (왜: 평균 기울기를 쓰려면 샘플 수로 나눌 수도 있지만, 여기선 합 그대로 사용)
W2 -= lr * (h.T @ delta2)
b2 -= lr * np.sum(delta2, axis=0, keepdims=True)
W1 -= lr * (X.T @ delta1)
b1 -= lr * np.sum(delta1, axis=0, keepdims=True)
if epoch % 1000 == 0:
print(f"epoch {epoch:4d} loss={loss:.5f}")
# 최종 예측
h = sigmoid(X @ W1 + b1)
y_hat = sigmoid(h @ W2 + b2)
print("\n최종 예측 (반올림):")
print(np.round(y_hat, 3))
print("정답:")
print(y.flatten())
epoch 0 loss=0.12829
epoch 1000 loss=0.04127
epoch 2000 loss=0.00512
epoch 3000 loss=0.00188
epoch 4000 loss=0.00110
최종 예측 (반올림):
[[0.035]
[0.961]
[0.963]
[0.041]]
정답:
[0 1 1 0]

0에 가까워야 할 값은 0.035, 0.041로 내려가고, 1에 가까워야 할 값은 0.961, 0.963으로 올라갔습니다. 단층 퍼셉트론이 못 풀던 XOR을, 2층 신경망 + 역전파가 해냈습니다.


손실이 줄어드는 과정을 눈으로 확인해 보겠습니다. 위 코드 끝에 다음을 이어 붙이면 됩니다.

plt.figure(figsize=(8, 4))
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('XOR 학습 곡선')
plt.yscale('log') # 로그 스케일로 보면 초반 급락이 잘 보임
plt.grid(True)
plt.show()

전형적으로 초반에는 거의 평평하다가(정체기), 어느 순간 뚝 떨어지고(돌파기), 이후 천천히 수렴하는 S자 곡선이 나타납니다. 이 ‘평평한 구간’이 바로 1970년대 연구자들이 “신경망은 학습이 안 된다”고 포기했던 지점입니다. 1986년 역전파 논문이 인내심을 가지고 충분한 에포크를 돌리면 돌파할 수 있음을 보였습니다.


⚠️ 에러 경험: 이 코드는 왜 망가질까?

섹션 제목: “⚠️ 에러 경험: 이 코드는 왜 망가질까?”

아래는 자주 저지르는 실수입니다. 실행하면 어떤 문제가 생길지 먼저 생각해 보겠습니다.

# 가중치를 모두 0으로 초기화
W1 = np.zeros((2, 4))
W2 = np.zeros((4, 1))
# (이후 코드 동일)

실행하면 에러는 나지 않습니다. 그런데 5000 에포크를 돌려도 손실이 거의 줄지 않습니다.

epoch 0 loss=0.12500
epoch 1000 loss=0.12500
epoch 2000 loss=0.12500

원인: 모든 가중치가 0이면 모든 은닉 뉴런이 똑같은 값을 출력합니다. 역전파로 계산되는 기울기도 모든 뉴런에 대해 동일합니다. 결국 4개의 뉴런이 하나의 뉴런과 똑같이 행동합니다. 이것을 대칭성 문제(symmetry breaking failure)라고 부릅니다.

수정: 처음처럼 np.random.randn(...) * 0.5로 작은 랜덤값을 주면 각 뉴런이 다르게 출발해 서로 다른 특징을 학습합니다.


학습률을 바꿔가며 같은 코드를 돌려 보겠습니다.

학습률 lr5000 에포크 후 손실관찰
0.001약 0.125거의 학습 안 됨 (정체)
0.1약 0.004느리지만 학습됨
0.5약 0.001빠르고 안정적 수렴
5.00.06 내외에서 진동골짜기 주변에서 튐
50.0NaN (발산)exp() 오버플로우

직접 lr 값만 바꿔 실행해 보면, 학습률이 단순한 숫자 하나가 아니라 학습의 성패를 가르는 하이퍼파라미터라는 사실을 몸으로 이해하게 됩니다.


활동 유형: 짝 활동 (2인 1조, 10분)

어떤 학생이 수학 시험에서 50점을 받았습니다. 이 50점이라는 ‘오차’의 원인을 거꾸로 추적한다고 가정합니다.

  • 어제 공부 시간이 부족했다 (요인 A)
  • 문제집 난이도가 안 맞았다 (요인 B)
  • 시험장에서 긴장했다 (요인 C)

각 요인이 50점이라는 결과에 ‘얼마나 책임이 있는지’ 정확히 계산할 수 있을까요? 짝과 함께 아래 질문을 토론하세요.

토론 질문

  1. 각 요인의 ‘책임 비율’을 수학적으로 계산하려면 어떤 정보가 필요합니까?
  2. 신경망의 가중치 하나하나에 대해 ‘오차에 대한 책임’을 계산하는 것과 어떤 점에서 비슷합니까?
  3. 만약 요인 A(공부 시간)가 요인 B(문제집)에 영향을 준다면, 단순히 A의 책임만 따로 볼 수 있을까요? (힌트: 연쇄법칙)

아래 코드에서 # TODO 부분을 채워 평균절대오차(MAE)를 계산하세요. MSE와 결과를 비교해 보세요.

import numpy as np
y = np.array([0, 1, 1, 0])
y_hat = np.array([0.1, 0.9, 0.8, 0.2])
# MSE
mse = 0.5 * np.mean((y_hat - y) ** 2)
# MAE (TODO: 절댓값 사용)
mae = None # 여기를 채우세요
print(f"MSE={mse:.4f}, MAE={mae:.4f}")
힌트

np.abs() 함수를 사용하세요. MAE는 제곱 대신 절댓값을 씁니다.

정답
mae = np.mean(np.abs(y_hat - y))

MSE는 큰 오차에 더 큰 벌점을 주고, MAE는 모든 오차에 동등하게 벌점을 줍니다.


🟡 응용: AND 게이트 학습시키기

섹션 제목: “🟡 응용: AND 게이트 학습시키기”

XOR 코드를 복사해서 AND 게이트를 학습시키세요. 데이터만 바꾸면 됩니다.

x1x2AND
000
010
100
111
힌트

y = np.array([[0], [0], [0], [1]])로 바꾸고 그대로 실행하세요. XOR보다 훨씬 빨리(수백 에포크 이내) 수렴합니다. 왜일까요? AND는 직선 하나로 분리 가능한 문제이기 때문입니다.

정답 코드
import numpy as np
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [0], [0], [1]]) # <- 여기만 변경
def sigmoid(z): return 1 / (1 + np.exp(-z))
def sigmoid_deriv(a): return a * (1 - a)
np.random.seed(42)
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))
for epoch in range(3000):
h = sigmoid(X @ W1 + b1)
y_hat = sigmoid(h @ W2 + b2)
delta2 = (y_hat - y) * sigmoid_deriv(y_hat)
delta1 = (delta2 @ W2.T) * sigmoid_deriv(h)
W2 -= 0.5 * (h.T @ delta2)
b2 -= 0.5 * np.sum(delta2, axis=0, keepdims=True)
W1 -= 0.5 * (X.T @ delta1)
b1 -= 0.5 * np.sum(delta1, axis=0, keepdims=True)
print(np.round(sigmoid(sigmoid(X @ W1 + b1) @ W2 + b2), 2))

🔴 도전: 수치 미분으로 역전파 검증하기

섹션 제목: “🔴 도전: 수치 미분으로 역전파 검증하기”

역전파로 계산한 $\dfrac{\partial L}{\partial W_2}$가 정말 맞는지 수치 미분으로 검증하세요. 수치 미분은 정의 그대로:

$ \dfrac{\partial L}{\partial w} \approx \dfrac{L(w + \epsilon) - L(w - \epsilon)}{2\epsilon} $

역전파 값과 수치 미분 값의 차이가 $10^{-7}$ 이하라면 구현이 맞습니다. 이를 기울기 체크(gradient check)라고 부르며, 실무 구현 시 필수 검증 절차입니다.

힌트
  1. 한 번 순전파해서 y_hat을 얻고, loss_original을 기록하세요.
  2. W2[0, 0]+eps 한 뒤 다시 순전파해서 loss_plus를 얻으세요.
  3. W2[0, 0]-eps 한 뒤 다시 순전파해서 loss_minus를 얻으세요.
  4. (loss_plus - loss_minus) / (2 * eps)가 수치 미분 결과입니다.
  5. 역전파로 구한 dW2[0, 0]과 비교하세요.
정답 코드
import numpy as np
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
def sigmoid(z): return 1 / (1 + np.exp(-z))
def sigmoid_deriv(a): return a * (1 - a)
np.random.seed(42)
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))
def forward_loss(W1, b1, W2, b2):
h = sigmoid(X @ W1 + b1)
y_hat = sigmoid(h @ W2 + b2)
return 0.5 * np.sum((y_hat - y) ** 2)
# 역전파로 기울기 계산
h = sigmoid(X @ W1 + b1)
y_hat = sigmoid(h @ W2 + b2)
delta2 = (y_hat - y) * sigmoid_deriv(y_hat)
dW2_backprop = h.T @ delta2
# 수치 미분으로 dW2[0,0] 검증
eps = 1e-5
W2_plus = W2.copy(); W2_plus[0, 0] += eps
W2_minus = W2.copy(); W2_minus[0, 0] -= eps
dW2_numeric = (forward_loss(W1, b1, W2_plus, b2)
- forward_loss(W1, b1, W2_minus, b2)) / (2 * eps)
print(f"역전파: {dW2_backprop[0, 0]:.8f}")
print(f"수치미분: {dW2_numeric:.8f}")
print(f"차이: {abs(dW2_backprop[0, 0] - dW2_numeric):.2e}")

출력 예시:

역전파: 0.12345678
수치미분: 0.12345679
차이: 1.23e-08

차이가 $10^{-8}$ 수준이면 역전파 구현이 올바릅니다.


직접 구현한 이 코드를 PyTorch로 쓰면 다음과 같습니다.

import torch
import torch.nn as nn
X = torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]])
y = torch.tensor([[0.],[1.],[1.],[0.]])
model = nn.Sequential(nn.Linear(2, 4), nn.Sigmoid(),
nn.Linear(4, 1), nn.Sigmoid())
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
loss_fn = nn.MSELoss()
for epoch in range(5000):
y_hat = model(X) # 순전파
loss = loss_fn(y_hat, y)
optimizer.zero_grad()
loss.backward() # <- 여기가 오늘 구현한 역전파
optimizer.step() # <- 여기가 경사하강 업데이트

loss.backward() 한 줄이 오늘 30줄로 직접 짠 역전파 로직을 자동으로 수행합니다. 이것이 자동 미분(autograd)입니다. PyTorch, TensorFlow, JAX 모두 내부적으로 연쇄법칙을 계산 그래프에 따라 자동 적용합니다.

실무 연결 포인트

  • GPT, BERT 같은 LLM: 수억~수천억 개 파라미터에 대해 똑같은 원리로 역전파가 일어납니다. 규모만 커졌을 뿐 수학은 동일합니다.
  • 이미지 분류(ResNet): 깊이 100층 이상에서도 연쇄법칙으로 기울기가 전달됩니다. 다만 층이 깊으면 기울기가 사라지는 문제(기울기 소실)가 생겨, 이를 해결하려 ReLU·잔차연결 같은 기법이 등장했습니다.
  • 디버깅: 모델이 학습이 안 될 때 가장 먼저 확인하는 것은 ‘기울기가 흐르고 있는가’입니다. param.grad를 찍어보는 습관이 필요합니다.


경사하강법에서 가중치를 업데이트하는 식 $w \leftarrow w - \eta \cdot \dfrac{\partial L}{\partial w}$에서 기울기 앞에 '마이너스(-)'가 붙는 이유로 가장 적절한 것은?

2층 신경망의 역전파에서 은닉층 오차 신호 $\delta_1 = (W_2^T \delta_2) \odot \sigma'(h)$의 의미로 가장 적절한 것은?

학습률(learning rate)을 50처럼 아주 큰 값으로 설정했을 때 가장 가능성이 높은 현상은?

XOR 문제가 단층 퍼셉트론으로는 풀리지 않지만 2층 신경망 + 역전파로 풀리는 근본적인 이유는?


서술형. “역전파는 연쇄법칙을 신경망에 적용한 것”이라는 말의 의미를, 2층 신경망을 예로 들어 세 문장 이내로 설명하세요. (힌트: 순전파 방향, 손실과 가중치의 관계, 연쇄법칙의 역할을 각각 한 문장씩)

예시 답안

2층 신경망의 순전파는 입력 → 은닉층 → 출력층 → 손실 순서로 합성함수처럼 여러 단계의 계산이 쌓여 일어납니다. 손실 $L$을 은닉층 가중치 $W_1$로 미분하려면 중간에 있는 출력 $\hat{y}$, 은닉 활성화 $h$ 등을 거쳐야 하므로 $\dfrac{\partial L}{\partial W_1}$을 직접 구할 수 없습니다. 역전파는 $\dfrac{\partial L}{\partial \hat{y}} \cdot \dfrac{\partial \hat{y}}{\partial h} \cdot \dfrac{\partial h}{\partial W_1}$처럼 연쇄법칙으로 미분을 층별로 곱해 내려오면서, 모든 가중치에 대한 기울기를 단 한 번의 역방향 계산으로 동시에 얻는 기법입니다.

핵심 포인트 3가지

  1. 신경망은 합성함수 구조이다 (순전파)
  2. 깊은 층의 가중치는 손실과 직접 연결되어 있지 않다
  3. 연쇄법칙으로 미분을 층별로 곱해 내려오면 모든 기울기를 효율적으로 얻을 수 있다

  • 연쇄법칙이 왜 역전파의 핵심 원리인지 내 말로 설명할 수 있다
  • 경사하강법이 손실 표면에서 최솟값을 찾는 과정을 ‘산에서 내려오기’ 비유로 설명할 수 있다
  • NumPy로 2층 신경망의 순전파와 역전파를 직접 구현할 수 있다
  • 학습률이 너무 크거나 작을 때 나타나는 현상을 예측할 수 있다
  • “신경망은 어떻게 스스로 배우는가?”라는 오늘의 질문에 내 답을 말할 수 있다

오늘 수업을 마치며 아래 질문에 스스로 답해 보세요. 정답은 없습니다. 생각의 흔적을 남기는 것이 중요합니다.

  • 역전파 수식을 처음 봤을 때와, XOR 신경망을 직접 짜 본 지금, 역전파에 대한 이해가 어떻게 달라졌습니까?
  • 학습률 실험에서 가장 인상 깊었던 순간은 언제였습니까? 만약 실제 프로젝트에서 학습이 안 된다면 무엇을 먼저 확인하겠습니까?
  • 사람이 실수를 통해 배우는 과정과 신경망의 역전파는 어떤 점에서 닮았고, 어떤 점에서 다릅니까?
  • 아직 납득되지 않거나 더 알고 싶은 부분이 있다면 무엇입니까? (예: 기울기 소실, 다른 활성화 함수, 자동 미분의 내부 동작 등)

7차시에서는 합성곱 신경망(CNN)을 다룹니다. 오늘 만든 완전연결 신경망은 입력이 4차원이면 쉽게 학습했지만, 28×28짜리 손글씨 이미지(784차원)는 파라미터가 폭발적으로 늘어납니다. CNN이 어떻게 ‘이미지의 공간 정보’를 살리면서 파라미터를 수십 배 줄이는지, 그리고 필터(kernel)가 어떻게 눈·귀·모서리 같은 특징을 스스로 찾아내는지 확인해 보겠습니다.

다음 질문: “여러분이 고양이 사진을 보고 0.1초 만에 ‘고양이다’라고 판단할 때, 뇌는 이미지의 어떤 부분을 먼저 보고 있을까요?”