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) ===
No description has been provided for this image
=== 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}")
No description has been provided for this image
최종 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("   비선형 결정 경계(곡선)가 두 클래스를 완벽히 분리합니다.")
No description has been provided for this image
✨ 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를 최소화하여 정확한 예측 만들기!