MLP로 XOR 문제 해결하기¶
1. 개요¶
XOR 문제는 단층 퍼셉트론으로는 해결할 수 없었던 비선형 분류 문제입니다. 이제 은닉층이 있는 다층 퍼셉트론(MLP)으로 이 문제를 해결해봅시다!
2. XOR 데이터 준비¶
XOR의 진리표를 학습 데이터로 만듭니다.
3. MLP 모델 정의¶
- 입력층: 2개 노드 (x1, x2)
- 은닉층: 4개 노드 (비선형 변환)
- 출력층: 1개 노드 (예측값)
- 활성화 함수: Sigmoid
4. 학습 과정¶
- 손실 함수: Binary Cross Entropy (BCE)
- 옵티마이저: Adam
- 학습률: 0.01
- 에포크: 10,000회
5. 결과 시각화¶
- 학습 곡선 (Loss 그래프)
- 결정 경계 (Decision Boundary)
- 예측 결과 테이블
In [1]:
import setup_env
--------------------------------------------------------------------------------
=== Hardware Acceleration ===
PyTorch version: 2.9.0a0+145a3a7bda.nv25.10
Using NVIDIA GPU (CUDA)
CUDA version: 13.0
GPU name: NVIDIA GeForce RTX 5070 Ti
GPU count: 1
Total GPU memory: 15.92 GB
Allocated memory: 0.00 GB
Free memory: 15.92 GB
Device: cuda
=== Matplotlib Settings ===
✅ Font: NanumGothic
=== System Info ===
OS: Ubuntu 24.04.3 LTS (Noble Numbat)
Kernel: 6.6.87.2-microsoft-standard-WSL2
Architecture: x86_64
Python: 3.12.3
Working directory: /workspace/ai-deeplearning/tutorial
=== Library Versions ===
NumPy: 2.1.0
Pandas: 3.0.0
Matplotlib: 3.10.7
Scikit-learn: 1.7.2
OpenCV: Not installed → !pip install -q opencv-python
Pillow: 12.0.0
Seaborn: 0.13.2
TensorFlow: Not installed → !pip install -q tensorflow
Transformers: 4.40.1
TorchVision: 0.24.0a0+094e7af5
=== Environment setup completed ===
--------------------------------------------------------------------------------
=== Visualizing Test Plot (Wide View) ===
=== GPU Usage Code Snippet === Device set to: cuda ---------------------------------------- # 아래 코드를 복사해서 모델과 데이터를 GPU로 보내세요: model = YourModel().to(device) data = data.to(device) ---------------------------------------- === Environment setup completed === --------------------------------------------------------------------------------
In [4]:
import torch
# ========================================
# 2. XOR 데이터 준비
# ========================================
# XOR 진리표
X = torch.tensor([[0, 0],
[0, 1],
[1, 0],
[1, 1]], dtype=torch.float32)
y = torch.tensor([[0],
[1],
[1],
[0]], dtype=torch.float32)
print("📊 XOR 데이터:")
print("입력(X):")
print(X.numpy())
print("\n출력(y):")
print(y.numpy())
📊 XOR 데이터: 입력(X): [[0. 0.] [0. 1.] [1. 0.] [1. 1.]] 출력(y): [[0.] [1.] [1.] [0.]]
In [6]:
import torch
import torch.nn as nn
# ========================================
# 3. MLP 모델 정의
# ========================================
class XOR_MLP(nn.Module):
def __init__(self):
super(XOR_MLP, self).__init__()
# 입력층(2) → 은닉층(4) → 출력층(1)
self.hidden = nn.Linear(2, 4) # 은닉층
self.output = nn.Linear(4, 1) # 출력층
self.sigmoid = nn.Sigmoid() # 활성화 함수
def forward(self, x):
x = self.sigmoid(self.hidden(x)) # 은닉층 통과
x = self.sigmoid(self.output(x)) # 출력층 통과
return x
# 모델 생성
model = XOR_MLP()
print("🧠 MLP 모델 구조:")
print(model)
🧠 MLP 모델 구조: XOR_MLP( (hidden): Linear(in_features=2, out_features=4, bias=True) (output): Linear(in_features=4, out_features=1, bias=True) (sigmoid): Sigmoid() )
In [8]:
import torch
import torch.nn as nn
import torch.optim as optim
# ========================================
# 4. 손실 함수와 옵티마이저 설정
# ========================================
criterion = nn.BCELoss() # Binary Cross Entropy Loss
optimizer = optim.Adam(model.parameters(), lr=0.01)
print("⚙️ 학습 설정:")
print(f"- 손실 함수: Binary Cross Entropy")
print(f"- 옵티마이저: Adam")
print(f"- 학습률: 0.01")
⚙️ 학습 설정: - 손실 함수: Binary Cross Entropy - 옵티마이저: Adam - 학습률: 0.01
In [9]:
import torch
import torch.nn as nn
import torch.optim as optim
# ========================================
# 5. 학습 진행
# ========================================
epochs = 10000
loss_history = []
print("🚀 학습 시작...\n")
for epoch in range(epochs):
# 순전파
outputs = model(X)
loss = criterion(outputs, y)
# 역전파 및 가중치 업데이트
optimizer.zero_grad() # 기울기 초기화
loss.backward() # 역전파
optimizer.step() # 가중치 업데이트
# 손실 기록
loss_history.append(loss.item())
# 1000 에포크마다 출력
if (epoch + 1) % 1000 == 0:
print(f"Epoch [{epoch+1:5d}/{epochs}] Loss: {loss.item():.6f}")
print("\n✅ 학습 완료!")
🚀 학습 시작... Epoch [ 1000/10000] Loss: 0.024574 Epoch [ 2000/10000] Loss: 0.007125 Epoch [ 3000/10000] Loss: 0.003157 Epoch [ 4000/10000] Loss: 0.001631 Epoch [ 5000/10000] Loss: 0.000906 Epoch [ 6000/10000] Loss: 0.000524 Epoch [ 7000/10000] Loss: 0.000309 Epoch [ 8000/10000] Loss: 0.000185 Epoch [ 9000/10000] Loss: 0.000111 Epoch [10000/10000] Loss: 0.000067 ✅ 학습 완료!
In [10]:
# ========================================
# 6. 학습 결과 확인
# ========================================
with torch.no_grad():
predictions = model(X)
print("\n📋 예측 결과:")
print("=" * 40)
for i in range(len(X)):
x1, x2 = X[i].numpy()
pred = predictions[i].item()
actual = y[i].item()
result = "✅" if round(pred) == actual else "❌"
print(f"입력: [{x1:.0f}, {x2:.0f}] → 예측: {pred:.4f} (실제: {actual:.0f}) {result}")
📋 예측 결과: ======================================== 입력: [0, 0] → 예측: 0.0000 (실제: 0) ✅ 입력: [0, 1] → 예측: 0.9999 (실제: 1) ✅ 입력: [1, 0] → 예측: 0.9999 (실제: 1) ✅ 입력: [1, 1] → 예측: 0.0001 (실제: 0) ✅
In [12]:
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
# ========================================
# 7. Loss 그래프 시각화
# ========================================
# 너비 100%로 표시되도록 큰 figsize 설정
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(loss_history, color='#58a6ff', linewidth=2)
ax.set_xlabel('Epoch', fontsize=12, fontweight='bold')
ax.set_ylabel('Loss (BCE)', fontsize=12, fontweight='bold')
ax.set_title('XOR 학습 곡선 (Loss vs Epoch)', fontsize=14, fontweight='bold', pad=20)
ax.grid(True, alpha=0.3, linestyle='--')
ax.set_xlim(0, epochs)
# 배경색 설정
ax.set_facecolor('#f8f9fa')
fig.patch.set_facecolor('white')
plt.tight_layout()
plt.show()
print(f"\n최종 Loss: {loss_history[-1]:.6f}")
최종 Loss: 0.000067
In [14]:
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
# ========================================
# 8. 결정 경계 시각화
# ========================================
# 너비 100%로 표시되도록 큰 figsize 설정
fig, ax = plt.subplots(figsize=(16, 12))
# 격자 생성 (해상도 높임)
x_min, x_max = -0.5, 1.5
y_min, y_max = -0.5, 1.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
np.linspace(y_min, y_max, 200))
# 모든 격자점에 대해 예측
grid_points = torch.tensor(np.c_[xx.ravel(), yy.ravel()], dtype=torch.float32)
with torch.no_grad():
Z = model(grid_points).numpy().reshape(xx.shape)
# 결정 경계 그리기 (등고선)
contour = ax.contourf(xx, yy, Z, levels=20, cmap='RdYlBu_r', alpha=0.8)
ax.contour(xx, yy, Z, levels=[0.5], colors='black', linewidths=3, linestyles='--')
# 컬러바 추가
cbar = plt.colorbar(contour, ax=ax)
cbar.set_label('예측 확률', fontsize=11, fontweight='bold')
# 실제 데이터 포인트 표시
colors = ['#ff6b6b', '#4ecdc4']
labels = ['클래스 0', '클래스 1']
for i in range(len(X)):
x1, x2 = X[i].numpy()
label = int(y[i].item())
ax.scatter(x1, x2, c=colors[label], s=400,
edgecolors='white', linewidths=3,
marker='o', zorder=10, label=labels[label] if i == 0 or i == 1 else "")
# 데이터 포인트에 좌표 표시
for i in range(len(X)):
x1, x2 = X[i].numpy()
ax.text(x1, x2, f'({x1:.0f},{x2:.0f})',
ha='center', va='center', fontsize=11,
fontweight='bold', color='white')
# 그래프 설정
ax.set_xlabel('x₁', fontsize=13, fontweight='bold')
ax.set_ylabel('x₂', fontsize=13, fontweight='bold')
ax.set_title('MLP 결정 경계 - XOR 문제 해결!', fontsize=15, fontweight='bold', pad=20)
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
ax.grid(True, alpha=0.3, linestyle='--')
ax.legend(loc='upper right', fontsize=11, framealpha=0.9)
# 배경색
ax.set_facecolor('#f8f9fa')
fig.patch.set_facecolor('white')
plt.tight_layout()
plt.show()
print("\n✨ MLP가 XOR 문제를 성공적으로 해결했습니다!")
print(" 비선형 결정 경계(곡선)가 두 클래스를 완벽히 분리합니다.")
✨ MLP가 XOR 문제를 성공적으로 해결했습니다! 비선형 결정 경계(곡선)가 두 클래스를 완벽히 분리합니다.
In [15]:
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
# ========================================
# 9. 모델 파라미터 확인
# ========================================
print("🔍 학습된 모델 파라미터:")
print("=" * 50)
for name, param in model.named_parameters():
print(f"\n{name}:")
print(param.data)
total_params = sum(p.numel() for p in model.parameters())
print(f"\n총 파라미터 수: {total_params}개")
🔍 학습된 모델 파라미터:
==================================================
hidden.weight:
tensor([[ -6.5091, -6.3733],
[ 7.1863, 6.6578],
[ -8.3982, 11.6573],
[-11.1185, 7.8757]])
hidden.bias:
tensor([ 0.3911, -0.7449, 2.9399, -2.8137])
output.weight:
tensor([[ -4.0079, 6.1181, -18.5819, 18.8931]])
output.bias:
tensor([3.4459])
총 파라미터 수: 17개
분류 문제의 손실 함수 (Loss Function)¶
📌 손실 함수란?¶
손실 함수는 모델의 예측이 실제 정답과 얼마나 다른지를 측정하는 함수입니다. 학습의 목표는 이 손실(Loss)을 최소화하는 것입니다!
🎯 1. Binary Cross Entropy (BCE) - 이진 분류¶
언제 사용하나요?¶
- 2개의 클래스를 분류할 때 (0 또는 1)
- 예: XOR 문제, 스팸/정상 메일, 합격/불합격
PyTorch 코드¶
criterion = nn.BCELoss()
특징¶
- 출력층 활성화 함수: Sigmoid (0~1 사이 확률값)
- 출력 해석: 0.8 → 80% 확률로 클래스 1
수식¶
$$\text{BCE} = -\frac{1}{N}\sum_{i=1}^{N}[y_i \log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i)]$$
- $y_i$: 실제 정답 (0 또는 1)
- $\hat{y}_i$: 모델의 예측 확률 (0~1)
🎨 2. Cross Entropy Loss (CE) - 다중 클래스 분류¶
언제 사용하나요?¶
- 3개 이상의 클래스를 분류할 때
- 예: 개/고양이/새 분류, MNIST 숫자 인식 (0~9), ImageNet (1000개 클래스)
PyTorch 코드¶
criterion = nn.CrossEntropyLoss()
특징¶
- 출력층 활성화 함수: Softmax (자동 포함됨!)
- 모든 클래스 확률의 합 = 1
- 출력 해석: [0.7, 0.2, 0.1] → 70% 개, 20% 고양이, 10% 새
수식¶
$$\text{CE} = -\sum_{c=1}^{C} y_c \log(\hat{y}_c)$$
- $C$: 클래스 개수
- $y_c$: 실제 정답 (one-hot encoding)
- $\hat{y}_c$: 각 클래스에 대한 예측 확률
📊 3. 비교표¶
| 손실 함수 | 클래스 수 | 출력층 활성화 | 출력 형태 | 사용 예시 |
|---|---|---|---|---|
| BCELoss | 2개 (이진) | Sigmoid | 단일 확률값 | XOR, 스팸 필터 |
| CrossEntropyLoss | 3개 이상 | Softmax | 클래스별 확률 | 개/고양이/새, MNIST |
💡 4. 엔트로피(Entropy)의 의미¶
엔트로피란?¶
정보 이론에서 불확실성을 측정하는 지표입니다.
- 낮은 엔트로피: 확신이 강함 → [0.99, 0.01] "거의 확실히 개다!"
- 높은 엔트로피: 불확실함 → [0.5, 0.5] "개인지 고양이인지 모르겠다..."
Cross Entropy의 의미¶
모델의 예측 분포와 실제 분포 사이의 차이를 측정합니다.
- 예측이 정답과 가까우면 → Loss 작음 ✅
- 예측이 정답과 멀면 → Loss 큼 ❌
🔧 5. XOR 문제에 적용¶
XOR은 이진 분류 문제이므로 BCELoss를 사용합니다:
# 모델 정의
class XOR_MLP(nn.Module):
def __init__(self):
super(XOR_MLP, self).__init__()
self.hidden = nn.Linear(2, 4)
self.output = nn.Linear(4, 1)
self.sigmoid = nn.Sigmoid() # ✅ 이진 분류용
def forward(self, x):
x = self.sigmoid(self.hidden(x))
x = self.sigmoid(self.output(x)) # 0~1 확률값 출력
return x
# 손실 함수
criterion = nn.BCELoss() # ✅ Binary Cross Entropy
# 학습
outputs = model(X)
loss = criterion(outputs, y) # 예측과 정답의 차이 계산
📌 6. 정리¶
| 항목 | 이진 분류 (XOR) | 다중 분류 (개/고양이/새) |
|---|---|---|
| 손실 함수 | BCELoss | CrossEntropyLoss |
| 출력층 | Sigmoid | Softmax (자동) |
| 출력 개수 | 1개 (확률) | 클래스 개수만큼 |
| 출력 범위 | 0~1 | 각각 0~1, 합=1 |
| 예측 방법 | round(출력) | argmax(출력) |
🎯 핵심 요약¶
- 이진 분류 (0/1):
nn.BCELoss()+Sigmoid - 다중 분류 (여러 클래스):
nn.CrossEntropyLoss()(Softmax 포함) - Cross Entropy: 예측과 정답의 차이를 "정보량"으로 측정
- 목표: Loss를 최소화하여 정확한 예측 만들기!