
📓Google Colab에서 실습하기
이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.
학습 내용
역전파 알고리즘 (Backpropagation)
학습 목표
이 레슨을 완료하면:
- •역전파가 왜 필요한지 직관적으로 이해합니다
- •연쇄 법칙(Chain Rule)을 신경망에 적용할 수 있습니다
- •2층 신경망의 역전파를 손으로 계산할 수 있습니다
- •numpy로 역전파를 직접 구현하고 XOR 문제를 풀 수 있습니다
- •계산 그래프 개념과 역전파의 효율성을 설명할 수 있습니다
핵심 메시지
"역전파는 공장 조립 라인의 품질 검사와 같습니다" 불량품이 나왔을 때, 어느 공정에서 문제가 생겼는지 끝에서부터 역추적하는 것이죠.
1. 비유: 공장 조립 라인의 품질 검사
자동차 공장을 상상해 보세요. 완성된 차에 결함이 발견되었습니다.
| 공정 | 공장 비유 | 신경망 대응 |
|---|---|---|
| 1단계 | 철판 절단 | 입력층 (데이터 받기) |
| 2단계 | 프레임 조립 | 은닉층 1 (특징 추출) |
| 3단계 | 도색 | 은닉층 2 (특징 조합) |
| 4단계 | 최종 검사 | 출력층 (예측 생성) |
| 불량 발견! | "문이 안 닫혀!" | 손실(Loss) 계산 |
품질 검사관은 끝에서부터 역추적합니다:
- •최종 검사 -> 도색 문제? -> 프레임 문제? -> 철판 문제?
- •각 공정이 불량에 얼마나 기여했는지 파악합니다
역전파도 똑같습니다. 출력의 오차를 뒤에서 앞으로 전파하면서, 각 가중치가 오차에 얼마나 기여했는지 계산합니다.
2. 순전파 복습: 앞으로 가는 계산
역전파를 이해하려면 먼저 순전파를 확실히 알아야 합니다.
| 단계 | 수식 | 의미 |
|---|---|---|
| 선형 변환 | z = W * x + b | 가중치를 곱하고 편향 더하기 |
| 활성화 | a = f(z) | 비선형 변환 적용 |
| 손실 계산 | L = loss(y_pred, y_true) | 예측과 정답의 차이 |
순전파는 입력 -> 은닉층 -> 출력 -> 손실 순서로 진행됩니다.
실행해보기: 순전파 직접 계산
pythonimport numpy as np # === 간단한 2층 신경망 순전파 === # 입력 x = np.array([[0.5], [0.3]]) # 2x1 print("입력 x:") print(x.T) # 은닉층 (2 뉴런) W1 = np.array([[0.4, 0.5], [0.6, 0.7]]) # 2x2 b1 = np.array([[0.1], [0.2]]) # 2x1 z1 = W1 @ x + b1 # 선형 변환 a1 = np.maximum(0, z1) # ReLU 활성화 print("\n은닉층 z1 (선형):", z1.T) print("은닉층 a1 (ReLU):", a1.T) # 출력층 (1 뉴런) W2 = np.array([[0.8, 0.9]]) # 1x2 b2 = np.array([[0.1]]) # 1x1 z2 = W2 @ a1 + b2 y_pred = 1 / (1 + np.exp(-z2)) # Sigmoid 활성화 print("\n출력 z2 (선형):", z2[0][0]) print("예측 y_pred (Sigmoid):", round(y_pred[0][0], 4)) # 손실 계산 (MSE) y_true = 1.0 loss = 0.5 * (y_pred[0][0] - y_true) ** 2 print("\n정답:", y_true) print("손실(MSE):", round(loss, 6))
3. 역전파의 핵심: 연쇄 법칙 (Chain Rule)
역전파의 수학적 기반은 미적분의 연쇄 법칙입니다.
비유: 도미노를 생각해 보세요. 첫 번째 도미노를 1cm 밀면, 두 번째는 2cm, 세 번째는 6cm 움직인다면, 첫 번째가 세 번째에 미치는 영향 = 2 x 3 = 6배 입니다.
수학으로 표현하면:
일 때,
즉, "최종 변화 = 각 단계의 변화를 곱한 것"
신경망에서는 이렇게 됩니다:
| 계산 방향 | 수식 | 의미 |
|---|---|---|
| 출력 -> 손실 | dL/dy_pred | 예측이 손실에 미치는 영향 |
| 활성화 -> 출력 | dy_pred/dz2 | 선형값이 예측에 미치는 영향 |
| 가중치 -> 활성화 | dz2/dW2 | 가중치가 선형값에 미치는 영향 |
| 연쇄 | dL/dW2 = dL/dy_pred * dy_pred/dz2 * dz2/dW2 | 모두 곱하면 = 가중치가 손실에 미치는 영향! |
4. 손으로 계산하는 역전파
아까 순전파에서 사용한 값들로 역전파를 직접 해봅시다.
| 단계 | 수식 | 설명 |
|---|---|---|
| Step 1 | → | 손실의 미분 (출발점) |
| Step 2 | → | Sigmoid 미분 |
| Step 3 | → | 출력층 가중치 미분 |
| Step 4 | 연쇄 법칙으로 조합 → W2의 기울기! |
실행해보기: 손계산을 코드로 검증
pythonimport numpy as np # 순전파 값들 (위에서 계산한 것) x = np.array([[0.5], [0.3]]) W1 = np.array([[0.4, 0.5], [0.6, 0.7]]) b1 = np.array([[0.1], [0.2]]) W2 = np.array([[0.8, 0.9]]) b2 = np.array([[0.1]]) y_true = 1.0 # --- 순전파 --- z1 = W1 @ x + b1 a1 = np.maximum(0, z1) z2 = W2 @ a1 + b2 y_pred = 1 / (1 + np.exp(-z2)) print("=== 순전파 결과 ===") print("y_pred:", round(y_pred[0][0], 4)) # --- 역전파 (손계산을 코드로!) --- print("\n=== 역전파 단계별 ===") # Step 1: 손실 미분 dL_dy = y_pred - y_true print("Step 1) dL/dy_pred:", round(dL_dy[0][0], 4)) # Step 2: Sigmoid 미분 dy_dz2 = y_pred * (1 - y_pred) print("Step 2) dy/dz2 (sigmoid 미분):", round(dy_dz2[0][0], 4)) # Step 3: 출력층 기울기 delta2 = dL_dy * dy_dz2 dL_dW2 = delta2 @ a1.T dL_db2 = delta2 print("Step 3) delta2 (출력 오차):", round(delta2[0][0], 4)) print(" dL/dW2:", np.round(dL_dW2, 4)) # Step 4: 은닉층으로 전파 dL_da1 = W2.T @ delta2 print("\nStep 4) 은닉층으로 전파된 오차:", np.round(dL_da1.T, 4)) # Step 5: ReLU 미분 relu_mask = (z1 > 0).astype(float) delta1 = dL_da1 * relu_mask print("Step 5) ReLU 마스크:", relu_mask.T) print(" delta1:", np.round(delta1.T, 4)) # Step 6: 은닉층 가중치 기울기 dL_dW1 = delta1 @ x.T dL_db1 = delta1 print("\nStep 6) dL/dW1:") print(np.round(dL_dW1, 4)) print(" dL/db1:", np.round(dL_db1.T, 4)) print("\n모든 가중치의 기울기를 구했습니다!") print("이 기울기로 가중치를 업데이트하면 학습이 됩니다.")
5. 계산 그래프로 이해하기
계산 그래프는 순전파의 모든 연산을 노드로 표현한 것입니다. 역전파는 이 그래프를 역방향으로 따라가며 기울기를 계산합니다.
| 순전파 연산 | 역전파 규칙 | 설명 |
|---|---|---|
| c = a + b | dc/da = 1, dc/db = 1 | 덧셈: 기울기를 그대로 전달 |
| c = a * b | dc/da = b, dc/db = a | 곱셈: 서로의 값을 전달 |
| c = f(a) | dc/da = f'(a) | 함수: 도함수를 곱함 |
핵심 통찰: 역전파에서 각 노드는 "내가 받은 기울기"에 "내 지역 기울기"를 곱해서 이전 노드에 전달합니다. 이것이 연쇄 법칙의 실제 구현입니다!
6. 역전파의 효율성: O(N) vs O(N^2)
왜 역전파가 획기적인 알고리즘일까요?
| 방법 | 시간 복잡도 | 설명 |
|---|---|---|
| 수치 미분 (naive) | O(N^2) | 파라미터 하나씩 살짝 바꿔서 기울기 측정. N개 파라미터마다 순전파 1회 |
| 역전파 | O(N) | 순전파 1회 + 역전파 1회로 모든 기울기를 한번에 계산! |
GPT-3는 파라미터가 1,750억 개입니다. 수치 미분으로는 1,750억 번 순전파를 해야 하지만, 역전파로는 단 1번의 순전파 + 1번의 역전파로 끝납니다!
🔬 실습: 수치 미분 vs 역전파 비교
두 방법으로 같은 기울기를 구하고, 역전파가 정확한지 검증해봅시다.
pythonimport numpy as np # ═══════════════════════════════════════════════════════════════ # 📊 수치 미분 vs 역전파 비교 # 두 방법의 결과가 같으면 역전파 구현이 정확! # ═══════════════════════════════════════════════════════════════ def sigmoid(z): return 1 / (1 + np.exp(-z)) # 📌 신경망 파라미터 설정 x_val = 0.5 # 입력 w1, b1 = 0.8, 0.1 # 1층 가중치, 편향 w2, b2 = 0.6, 0.2 # 2층 가중치, 편향 y_true = 1.0 # 정답 eps = 1e-5 # 수치 미분용 작은 값 params = {'w1': w1, 'b1': b1, 'w2': w2, 'b2': b2} # 📐 순전파 함수 def forward(w1, b1, w2, b2): z1 = w1 * x_val + b1 # 1층 선형변환 a1 = max(0, z1) # ReLU 활성화 z2 = w2 * a1 + b2 # 2층 선형변환 y_pred = sigmoid(z2) # Sigmoid 출력 return 0.5 * (y_pred - y_true) ** 2 # MSE 손실 # ───────────────────────────────────────────────────────────────── # 🐌 방법 1: 수치 미분 (느리지만 직관적) # 각 파라미터를 살짝 바꿔서 손실 변화 측정 # ───────────────────────────────────────────────────────────────── print("🐌 수치 미분 (파라미터마다 순전파 2회 필요)") print("=" * 50) num_grads = {} for name, val in params.items(): p_plus = dict(params); p_plus[name] = val + eps # 살짝 증가 p_minus = dict(params); p_minus[name] = val - eps # 살짝 감소 num_grads[name] = (forward(**p_plus) - forward(**p_minus)) / (2 * eps) print(f" ∂L/∂{name} = {num_grads[name]:.6f}") # ───────────────────────────────────────────────────────────────── # 🚀 방법 2: 역전파 (빠르고 효율적) # 연쇄법칙으로 한 번에 모든 기울기 계산 # ───────────────────────────────────────────────────────────────── print("\n🚀 역전파 (순전파 1회 + 역전파 1회)") print("=" * 50) # 순전파 z1 = w1 * x_val + b1 a1 = max(0, z1) z2 = w2 * a1 + b2 y_pred = sigmoid(z2) # 역전파 (출력 → 입력 방향으로) dL_dy = y_pred - y_true # 손실의 미분 dy_dz2 = y_pred * (1 - y_pred) # Sigmoid의 미분 delta2 = dL_dy * dy_dz2 # 연쇄법칙 적용 dL_dw2 = delta2 * a1 # w2의 기울기 dL_db2 = delta2 # b2의 기울기 dL_da1 = delta2 * w2 # a1으로 전파 dL_dz1 = dL_da1 * (1 if z1 > 0 else 0) # ReLU 미분 dL_dw1 = dL_dz1 * x_val # w1의 기울기 dL_db1 = dL_dz1 # b1의 기울기 bp_grads = {'w1': dL_dw1, 'b1': dL_db1, 'w2': dL_dw2, 'b2': dL_db2} for name, val in bp_grads.items(): print(f" ∂L/∂{name} = {val:.6f}") # ───────────────────────────────────────────────────────────────── # ✅ 검증: 두 방법의 결과 비교 # ───────────────────────────────────────────────────────────────── print("\n✅ 검증: 두 방법의 차이 (0에 가까우면 정확!)") print("=" * 50) for name in params: diff = abs(num_grads[name] - bp_grads[name]) status = "✅" if diff < 1e-6 else "❌" print(f" {name} 차이: {diff:.10f} {status}")
7. 실전: numpy로 XOR 문제 풀기
XOR은 선형으로 풀 수 없는 대표적인 문제입니다. 2층 신경망 + 역전파로 풀어봅시다!
| 입력 A | 입력 B | XOR 출력 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
🔬 실습: XOR 신경망 학습
단순 선형 모델로는 풀 수 없는 XOR 문제를 2층 신경망으로 해결해봅시다!
pythonimport numpy as np # ═══════════════════════════════════════════════════════════════ # 📊 XOR 문제 - 신경망으로 풀기 # XOR: 입력이 다르면 1, 같으면 0 # ═══════════════════════════════════════════════════════════════ # 📌 XOR 데이터 # (0,0)→0, (0,1)→1, (1,0)→1, (1,1)→0 X = np.array([[0,0],[0,1],[1,0],[1,1]]).T # 2×4 (입력 2개, 샘플 4개) Y = np.array([[0,1,1,0]]) # 1×4 (정답) print("📌 XOR 문제") print("입력 (X):", X.T.tolist()) print("정답 (Y):", Y.tolist()) # ───────────────────────────────────────────────────────────────── # 🧠 신경망 구조: 입력(2) → 은닉층(4) → 출력(1) # ───────────────────────────────────────────────────────────────── np.random.seed(42) W1 = np.random.randn(4, 2) * 0.5 # 1층 가중치 (4×2) b1 = np.zeros((4, 1)) # 1층 편향 (4×1) W2 = np.random.randn(1, 4) * 0.5 # 2층 가중치 (1×4) b2 = np.zeros((1, 1)) # 2층 편향 (1×1) lr = 1.0 # 학습률 def sigmoid(z): return 1 / (1 + np.exp(-np.clip(z, -500, 500))) # ───────────────────────────────────────────────────────────────── # 🚀 학습 시작! # ───────────────────────────────────────────────────────────────── losses = [] print("\n🚀 학습 시작...") for epoch in range(3000): # ═══ 순전파 ═══ Z1 = W1 @ X + b1 # 1층 선형변환 A1 = np.maximum(0, Z1) # ReLU 활성화 Z2 = W2 @ A1 + b2 # 2층 선형변환 A2 = sigmoid(Z2) # Sigmoid 출력 (확률) # ═══ 손실 계산 (Cross-Entropy) ═══ m = X.shape[1] # 샘플 수 loss = -np.mean(Y * np.log(A2 + 1e-8) + (1 - Y) * np.log(1 - A2 + 1e-8)) losses.append(loss) # ═══ 역전파 ═══ dZ2 = A2 - Y # 출력층 오차 dW2 = (1/m) * dZ2 @ A1.T # W2 기울기 db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True) # b2 기울기 dA1 = W2.T @ dZ2 # 은닉층으로 전파 dZ1 = dA1 * (Z1 > 0).astype(float) # ReLU 미분 dW1 = (1/m) * dZ1 @ X.T # W1 기울기 db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True) # b1 기울기 # ═══ 가중치 업데이트 ═══ W2 -= lr * dW2 b2 -= lr * db2 W1 -= lr * dW1 b1 -= lr * db1 if epoch % 500 == 0: print("Epoch " + str(epoch) + " | Loss: " + str(round(loss, 4))) print("\n=== 학습 완료! 예측 결과 ===") Z1 = W1 @ X + b1 A1 = np.maximum(0, Z1) Z2 = W2 @ A1 + b2 predictions = sigmoid(Z2) for i in range(4): inp = str(int(X[0,i])) + ", " + str(int(X[1,i])) pred = round(predictions[0,i], 3) label = int(Y[0,i]) result = "O" if round(pred) == label else "X" print("입력: [" + inp + "] -> 예측: " + str(pred) + " (정답: " + str(label) + ") " + result)
실행해보기: 학습 곡선 시각화
pythonimport numpy as np import matplotlib.pyplot as plt X = np.array([[0,0],[0,1],[1,0],[1,1]]).T Y = np.array([[0,1,1,0]]) np.random.seed(42) W1 = np.random.randn(4, 2) * 0.5 b1 = np.zeros((4, 1)) W2 = np.random.randn(1, 4) * 0.5 b2 = np.zeros((1, 1)) lr = 1.0 def sigmoid(z): return 1 / (1 + np.exp(-np.clip(z, -500, 500))) losses = [] for epoch in range(3000): Z1 = W1 @ X + b1 A1 = np.maximum(0, Z1) Z2 = W2 @ A1 + b2 A2 = sigmoid(Z2) m = X.shape[1] loss = -np.mean(Y * np.log(A2 + 1e-8) + (1 - Y) * np.log(1 - A2 + 1e-8)) losses.append(loss) dZ2 = A2 - Y dW2 = (1/m) * dZ2 @ A1.T db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True) dA1 = W2.T @ dZ2 dZ1 = dA1 * (Z1 > 0).astype(float) dW1 = (1/m) * dZ1 @ X.T db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True) W2 -= lr * dW2; b2 -= lr * db2; W1 -= lr * dW1; b1 -= lr * db1 plt.figure(figsize=(10, 5)) plt.plot(losses, linewidth=2, color='#2196F3') plt.xlabel('Epoch', fontsize=12) plt.ylabel('Loss (Cross-Entropy)', fontsize=12) plt.title('XOR Neural Network Training - Loss Curve', fontsize=14) plt.grid(True, alpha=0.3) plt.xlim(0, 3000) plt.tight_layout() plt.show() print("\nLoss:", round(losses[0], 4), "->", round(losses[-1], 4))
8. PyTorch autograd와의 연결
우리가 손으로 구현한 역전파를 PyTorch는 자동으로 해줍니다!
| 우리가 한 것 | PyTorch가 해주는 것 |
|---|---|
| 순전파에서 중간값 저장 (z1, a1 등) | requires_grad=True로 자동 추적 |
| 연쇄 법칙으로 기울기 계산 | loss.backward() 한 줄로 끝 |
| 기울기를 변수에 직접 저장 | param.grad에 자동 저장 |
| 가중치 업데이트 수식 직접 작성 | optimizer.step()으로 끝 |
python# ═══════════════════════════════════════════════════════════════ # ✨ PyTorch에서는 단 3줄! # ═══════════════════════════════════════════════════════════════ loss = criterion(model(x), y) # 순전파 loss.backward() # 역전파 (이 한 줄이 위의 모든 계산!) optimizer.step() # 가중치 업데이트
하지만 내부에서 일어나는 일은 우리가 구현한 것과 정확히 동일합니다. 원리를 알고 쓰면, 디버깅할 때 큰 차이를 만듭니다!
핵심 요약
| 개념 | 설명 | 비유 |
|---|---|---|
| 순전파 | 입력 -> 출력 계산 | 공장 조립 라인 |
| 손실 계산 | 예측과 정답의 차이 | 품질 검사 |
| 역전파 | 손실을 뒤에서 앞으로 전파 | 불량 원인 역추적 |
| 연쇄 법칙 | 각 단계의 미분을 곱함 | 도미노 효과 |
| 기울기 | 가중치가 손실에 미치는 영향 | 각 공정의 불량 기여도 |
| 가중치 업데이트 | w = w - lr * gradient | 공정 개선 |
학습 체크리스트
- • 역전파의 직관 이해 (공장 품질 검사 비유)
- • 연쇄 법칙이 왜 필요한지 설명 가능
- • 2층 신경망의 역전파를 단계별로 따라갈 수 있음
- • numpy로 순전파 + 역전파 구현 가능
- • 역전파가 O(N)인 이유 설명 가능
- • XOR 문제를 신경망으로 풀 수 있음
다음 강의 예고
"활성화 함수 심화" - Sigmoid, ReLU, GELU 등 활성화 함수의 세계를 깊이 탐구합니다!
레슨 정보
- 레벨
- Level 3: 딥러닝 핵심
- 예상 소요 시간
- 25분
- 참고 영상
- YouTube 링크
💡실습 환경 안내
이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.
Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.