# Part 7: 딥러닝 기초 실습 (PyTorch)

# 이 스크립트를 실행하기 전에 라이브러리를 설치해야 합니다.
# pip install torch torchvision scikit-learn

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_iris
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from torch.utils.data import DataLoader, TensorDataset

# --- 1. 데이터 준비 및 전처리 ---
print("--- 1. 데이터 준비 및 전처리 ---")
# Scikit-learn에 내장된 붓꽃(Iris) 데이터셋 로드
iris = load_iris()
X = iris.data
y = iris.target

# 훈련/테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 데이터 스케일링
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# PyTorch 텐서로 변환
X_train_tensor = torch.FloatTensor(X_train_scaled)
y_train_tensor = torch.LongTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test_scaled)
y_test_tensor = torch.LongTensor(y_test)

# DataLoader 생성
# 딥러닝 모델은 보통 미니배치 단위로 학습하므로 DataLoader를 사용
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(dataset=train_dataset, batch_size=16, shuffle=True)

print("PyTorch 텐서 및 DataLoader 준비 완료")
print(f"훈련 데이터 크기: {X_train_tensor.shape}")
print(f"훈련 타겟 크기: {y_train_tensor.shape}")
print("-" * 30)


# --- 2. 딥러NING 모델 정의 ---
print("\n--- 2. 딥러닝 모델 정의 ---")


class SimpleNN(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out


# 모델 파라미터 설정
input_size = X_train.shape[1]  # 특성의 수: 4
hidden_size = 32
num_classes = len(np.unique(y_train))  # 클래스의 수: 3
learning_rate = 0.01
num_epochs = 50

model = SimpleNN(input_size, hidden_size, num_classes)
print("모델 구조:")
print(model)
print("-" * 30)


# --- 3. 손실 함수 및 옵티마이저 정의 ---
print("\n--- 3. 손실 함수 및 옵티마이저 정의 ---")
# 다중 분류 문제이므로 CrossEntropyLoss 사용
criterion = nn.CrossEntropyLoss()
# Adam 옵티마이저 사용
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

print(f"손실 함수: {criterion}")
print(f"옵티마이저: {optimizer}")
print("-" * 30)


# --- 4. 모델 훈련 ---
print("\n--- 4. 모델 훈련 ---")
for epoch in range(num_epochs):
    for i, (features, labels) in enumerate(train_loader):
        # 순전파 (Forward pass)
        outputs = model(features)
        loss = criterion(outputs, labels)

        # 역전파 및 최적화 (Backward and optimize)
        optimizer.zero_grad()  # 이전 그래디언트 초기화
        loss.backward()  # 그래디언트 계산
        optimizer.step()  # 가중치 업데이트

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")
print("모델 훈련 완료")
print("-" * 30)


# --- 5. 모델 평가 ---
print("\n--- 5. 모델 평가 ---")
# 모델을 평가 모드로 설정 (dropout, batchnorm 등 비활성화)
model.eval()

# 테스트 데이터로 예측
with torch.no_grad():  # 그래디언트 계산 비활성화
    outputs = model(X_test_tensor)
    # 가장 높은 확률을 가진 클래스를 예측값으로 선택
    _, predicted = torch.max(outputs.data, 1)

accuracy = accuracy_score(y_test_tensor.numpy(), predicted.numpy())
print(f"테스트 데이터 정확도: {100 * accuracy:.2f}%")

# 일부 예측 결과 확인
print("\n일부 샘플 예측 결과:")
print(f"실제 값: {y_test_tensor[:10].numpy()}")
print(f"예측 값: {predicted[:10].numpy()}")
print("-" * 30)
