6차시: 역전파 - 신경망은 어떻게 스스로 배우는가
🎯 이 차시의 핵심 주제
섹션 제목: “🎯 이 차시의 핵심 주제”🧠 연쇄법칙과 역전파
섹션 제목: “🧠 연쇄법칙과 역전파”오차를 거꾸로 흘려보내 각 가중치의 ‘책임’을 계산하는 수학적 원리
💬 학습률의 딜레마
섹션 제목: “💬 학습률의 딜레마”너무 크면 발산, 너무 작으면 정체 — 적절한 보폭은 어떻게 정할까?
🔧 NumPy 2층 신경망
섹션 제목: “🔧 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) | 골짜기를 건너뛰어 발산 | 산을 뛰어넘는 거인 |
3. 연쇄법칙 — 역전파의 심장
섹션 제목: “3. 연쇄법칙 — 역전파의 심장”문제 하나를 풀어 보겠습니다. 빵 가격이 오르면 샌드위치 가격이 오르고, 샌드위치가 오르면 제 점심값이 오릅니다. 그렇다면 “빵 가격이 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년 힌튼·루멜하트·윌리엄스의 이 아이디어가 딥러닝을 가능하게 만든 핵심입니다.
4. 2층 신경망의 역전파 수식
섹션 제목: “4. 2층 신경망의 역전파 수식”이제 실제로 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인가?
섹션 제목: “왜 XOR인가?”XOR(배타적 논리합)은 입력 두 개가 다를 때만 1을 출력합니다.
| x1 | x2 | XOR |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
이 문제는 단층 퍼셉트론으로 절대 풀 수 없습니다(직선 하나로 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 -> 은닉 4b1 = np.zeros((1, 4))W2 = np.random.randn(4, 1) * 0.5 # 은닉 4 -> 출력 1b2 = np.zeros((1, 1))
# 순전파z1 = X @ W1 + b1 # 선형 조합h = sigmoid(z1) # 은닉층 출력z2 = h @ W2 + b2y_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.5b1 = np.zeros((1, 4))W2 = np.random.randn(4, 1) * 0.5b2 = np.zeros((1, 1))
lr = 0.1 # 학습률
# --- 순전파 ---z1 = X @ W1 + b1h = sigmoid(z1)z2 = h @ W2 + b2y_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/∂W2db2 = np.sum(delta2, axis=0, keepdims=True)
delta1 = (delta2 @ W2.T) * sigmoid_deriv(h) # <- 은닉층 오차 신호 (연쇄법칙)dW1 = X.T @ delta1 # <- ∂L/∂W1db1 = np.sum(delta1, axis=0, keepdims=True)
# --- 가중치 업데이트 (경사하강) ---W2 -= lr * dW2b2 -= lr * db2W1 -= lr * dW1b1 -= 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 npimport 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.5b1 = np.zeros((1, 4))W2 = np.random.randn(4, 1) * 0.5b2 = np.zeros((1, 1))
lr = 0.5epochs = 5000losses = [] # 학습 곡선용
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.12829epoch 1000 loss=0.04127epoch 2000 loss=0.00512epoch 3000 loss=0.00188epoch 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.12500epoch 1000 loss=0.12500epoch 2000 loss=0.12500원인: 모든 가중치가 0이면 모든 은닉 뉴런이 똑같은 값을 출력합니다. 역전파로 계산되는 기울기도 모든 뉴런에 대해 동일합니다. 결국 4개의 뉴런이 하나의 뉴런과 똑같이 행동합니다. 이것을 대칭성 문제(symmetry breaking failure)라고 부릅니다.
수정: 처음처럼 np.random.randn(...) * 0.5로 작은 랜덤값을 주면 각 뉴런이 다르게 출발해 서로 다른 특징을 학습합니다.
🔬 학습률 실험
섹션 제목: “🔬 학습률 실험”학습률을 바꿔가며 같은 코드를 돌려 보겠습니다.
학습률 lr | 5000 에포크 후 손실 | 관찰 |
|---|---|---|
| 0.001 | 약 0.125 | 거의 학습 안 됨 (정체) |
| 0.1 | 약 0.004 | 느리지만 학습됨 |
| 0.5 | 약 0.001 | 빠르고 안정적 수렴 |
| 5.0 | 0.06 내외에서 진동 | 골짜기 주변에서 튐 |
| 50.0 | NaN (발산) | exp() 오버플로우 |
직접 lr 값만 바꿔 실행해 보면, 학습률이 단순한 숫자 하나가 아니라 학습의 성패를 가르는 하이퍼파라미터라는 사실을 몸으로 이해하게 됩니다.
🤔 토론 / 탐구 활동
섹션 제목: “🤔 토론 / 탐구 활동”활동 유형: 짝 활동 (2인 1조, 10분)
활동: “책임 분배 토론”
섹션 제목: “활동: “책임 분배 토론””어떤 학생이 수학 시험에서 50점을 받았습니다. 이 50점이라는 ‘오차’의 원인을 거꾸로 추적한다고 가정합니다.
- 어제 공부 시간이 부족했다 (요인 A)
- 문제집 난이도가 안 맞았다 (요인 B)
- 시험장에서 긴장했다 (요인 C)
각 요인이 50점이라는 결과에 ‘얼마나 책임이 있는지’ 정확히 계산할 수 있을까요? 짝과 함께 아래 질문을 토론하세요.
토론 질문
- 각 요인의 ‘책임 비율’을 수학적으로 계산하려면 어떤 정보가 필요합니까?
- 신경망의 가중치 하나하나에 대해 ‘오차에 대한 책임’을 계산하는 것과 어떤 점에서 비슷합니까?
- 만약 요인 A(공부 시간)가 요인 B(문제집)에 영향을 준다면, 단순히 A의 책임만 따로 볼 수 있을까요? (힌트: 연쇄법칙)
🧩 연습 문제
섹션 제목: “🧩 연습 문제”🟢 기초: 손실 계산 변형
섹션 제목: “🟢 기초: 손실 계산 변형”아래 코드에서 # TODO 부분을 채워 평균절대오차(MAE)를 계산하세요. MSE와 결과를 비교해 보세요.
import numpy as npy = np.array([0, 1, 1, 0])y_hat = np.array([0.1, 0.9, 0.8, 0.2])
# MSEmse = 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 게이트를 학습시키세요. 데이터만 바꾸면 됩니다.
| x1 | x2 | AND |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
힌트
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.5b1 = np.zeros((1, 4))W2 = np.random.randn(4, 1) * 0.5b2 = 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)라고 부르며, 실무 구현 시 필수 검증 절차입니다.
힌트
- 한 번 순전파해서
y_hat을 얻고,loss_original을 기록하세요. W2[0, 0]만+eps한 뒤 다시 순전파해서loss_plus를 얻으세요.W2[0, 0]만-eps한 뒤 다시 순전파해서loss_minus를 얻으세요.(loss_plus - loss_minus) / (2 * eps)가 수치 미분 결과입니다.- 역전파로 구한
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.5b1 = np.zeros((1, 4))W2 = np.random.randn(4, 1) * 0.5b2 = 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-5W2_plus = W2.copy(); W2_plus[0, 0] += epsW2_minus = W2.copy(); W2_minus[0, 0] -= epsdW2_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 torchimport 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를 찍어보는 습관이 필요합니다.
🔗 참고 자료
섹션 제목: “🔗 참고 자료”📎 3Blue1Brown: Backpropagation Calculus
섹션 제목: “📎 3Blue1Brown: Backpropagation Calculus”역전파 수식을 시각적 애니메이션으로 유도하는 영상. 오늘 배운 연쇄법칙을 그림으로 다시 확인할 수 있습니다 →
📎 CS231n: Backpropagation Notes
섹션 제목: “📎 CS231n: Backpropagation Notes”스탠퍼드 대학의 공식 노트. 계산 그래프를 이용한 역전파 설명이 실무 구현 감각을 길러줍니다 →
📎 Rumelhart, Hinton, Williams (1986)
섹션 제목: “📎 Rumelhart, Hinton, Williams (1986)”“Learning representations by back-propagating errors”. 역전파의 원조 논문으로, 현대 딥러닝의 출발점입니다 →
📝 형성 평가
섹션 제목: “📝 형성 평가”경사하강법에서 가중치를 업데이트하는 식 $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가지
- 신경망은 합성함수 구조이다 (순전파)
- 깊은 층의 가중치는 손실과 직접 연결되어 있지 않다
- 연쇄법칙으로 미분을 층별로 곱해 내려오면 모든 기울기를 효율적으로 얻을 수 있다
자기점검 체크리스트
섹션 제목: “자기점검 체크리스트”- 연쇄법칙이 왜 역전파의 핵심 원리인지 내 말로 설명할 수 있다
- 경사하강법이 손실 표면에서 최솟값을 찾는 과정을 ‘산에서 내려오기’ 비유로 설명할 수 있다
- NumPy로 2층 신경망의 순전파와 역전파를 직접 구현할 수 있다
- 학습률이 너무 크거나 작을 때 나타나는 현상을 예측할 수 있다
- “신경망은 어떻게 스스로 배우는가?”라는 오늘의 질문에 내 답을 말할 수 있다
💭 성찰
섹션 제목: “💭 성찰”오늘 수업을 마치며 아래 질문에 스스로 답해 보세요. 정답은 없습니다. 생각의 흔적을 남기는 것이 중요합니다.
- 역전파 수식을 처음 봤을 때와, XOR 신경망을 직접 짜 본 지금, 역전파에 대한 이해가 어떻게 달라졌습니까?
- 학습률 실험에서 가장 인상 깊었던 순간은 언제였습니까? 만약 실제 프로젝트에서 학습이 안 된다면 무엇을 먼저 확인하겠습니까?
- 사람이 실수를 통해 배우는 과정과 신경망의 역전파는 어떤 점에서 닮았고, 어떤 점에서 다릅니까?
- 아직 납득되지 않거나 더 알고 싶은 부분이 있다면 무엇입니까? (예: 기울기 소실, 다른 활성화 함수, 자동 미분의 내부 동작 등)
🔗 다음 차시 미리보기
섹션 제목: “🔗 다음 차시 미리보기”7차시에서는 합성곱 신경망(CNN)을 다룹니다. 오늘 만든 완전연결 신경망은 입력이 4차원이면 쉽게 학습했지만, 28×28짜리 손글씨 이미지(784차원)는 파라미터가 폭발적으로 늘어납니다. CNN이 어떻게 ‘이미지의 공간 정보’를 살리면서 파라미터를 수십 배 줄이는지, 그리고 필터(kernel)가 어떻게 눈·귀·모서리 같은 특징을 스스로 찾아내는지 확인해 보겠습니다.
다음 질문: “여러분이 고양이 사진을 보고 0.1초 만에 ‘고양이다’라고 판단할 때, 뇌는 이미지의 어떤 부분을 먼저 보고 있을까요?”