🧩 MLP란?
퍼셉트론

퍼셉트론(Perceptron)은 1957년 프랑크 로젠블라트가 고안한 최초의 인공 신경망 알고리즘이다. 인간의 뇌 신경세포인 뉴런이 전기 신호를 전달하는 모습을 모방하여, 여러 개의 입력값을 받아 각각의 가중치를 곱하고 더한 뒤, 그 합이 임계치(역치)를 넘으면 1, 아니면 0(또는 -1)을 출력하는 방식이다.
MLP

MLP(Multi-Layer Perceptron)는 퍼셉트론을 여러 층(Layer)으로 쌓은 인공 신경망이다. 각 층에서는 활성함수를 통해 입력을 처리한다.
- 입력층 (Input Layer): 외부에서 데이터를 처음 받아들이는 층
- 은닉층 (Hidden Layer): 입력층과 출력층 사이에 위치하며, 복잡한 패턴과 비선형 관계를 찾아내는 핵심적인 층
- 출력층 (Output Layer): 은닉층에서 전달된 정보를 바탕으로 최종 결과나 예측값을 내보내는 층
딥러닝
Deep Learning은 은닉층을 깊게(Deep) 쌓은 인공신경망 전체를 뜻한다. 종류로는 CNN, RNN, Transformer 등이 있으며, MLP는 딥러닝의 아주 기초적인 형태이다.
🧩 MLP 구현하기
목표는 이전과 같다.
게임 시작 전 챔피언 픽, 밴, 소환사 주문 정보를 보고 팀1의 승리 여부를 예측한다.
필요한 라이브러리 세팅
import copy
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import accuracy_score, roc_auc_score
PyTorch에서 신경망 레이어는 보통 nn.Module을 상속해 만들며, nn.Linear는 입력에 선형 변환을 적용하는 레이어다.
재현성을 위한 시드(seed) 설정
신경망 학습은 초기 가중치와 데이터 셔플링 순서에 따라 결과가 조금씩 달라질 수 있다.
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(SEED)
데이터 불러오기
df = pd.read_csv("dataset/games.csv")
# 팀1 승리면 1, 팀2 승리면 0
df["target"] = (df["winner"] == 1).astype(int)
champ_cols = [
"t1_champ1id", "t1_champ2id", "t1_champ3id",
"t1_champ4id", "t1_champ5id",
"t2_champ1id", "t2_champ2id", "t2_champ3id",
"t2_champ4id", "t2_champ5id"
]
summoner_spell_cols = [
"t1_champ1_sum1", "t1_champ1_sum2",
"t1_champ2_sum1", "t1_champ2_sum2",
"t1_champ3_sum1", "t1_champ3_sum2",
"t1_champ4_sum1", "t1_champ4_sum2",
"t1_champ5_sum1", "t1_champ5_sum2",
"t2_champ1_sum1", "t2_champ1_sum2",
"t2_champ2_sum1", "t2_champ2_sum2",
"t2_champ3_sum1", "t2_champ3_sum2",
"t2_champ4_sum1", "t2_champ4_sum2",
"t2_champ5_sum1", "t2_champ5_sum2"
]
ban_cols = [
"t1_ban1", "t1_ban2", "t1_ban3", "t1_ban4", "t1_ban5",
"t2_ban1", "t2_ban2", "t2_ban3", "t2_ban4", "t2_ban5"
]
feature_cols = (
["seasonId"]
+ champ_cols
+ summoner_spell_cols
+ ban_cols
)
X = df[feature_cols]
y = df["target"]
이전과 마찬가지로 데이터 누수를 막기 위해 경기 종료 후에 알 수 있는 정보는 제외해야 한다.
# 사용하지 않는 예시
# gameDuration
# firstBlood
# t1_towerKills, t2_towerKills
# t1_dragonKills, t2_dragonKills
# ...
Train / Validation / Test 데이터 분리
- Train: 모델 가중치 학습용
- Validation: 학습 중 모델 설정 및 과적합 여부 확인용
- Test: 최종 성능 평가용
# 1차 분할: train 80%, test 20%
X_train_full, X_test, y_train_full, y_test = train_test_split(
X,
y,
test_size=0.2,
random_state=SEED,
stratify=y
)
# 2차 분할: train_full 중 일부를 validation으로 사용
# 최종 비율: train 64%, validation 16%, test 20%
X_train, X_val, y_train, y_val = train_test_split(
X_train_full,
y_train_full,
test_size=0.2,
random_state=SEED,
stratify=y_train_full
)
print("Train:", X_train.shape) # Train: (32953, 41)
print("Validation:", X_val.shape) # Validation: (8239, 41)
print("Test:", X_test.shape) # Test: (10298, 41)
One-Hot Encoding
ID값은 크기 자체에 의미가 없는 범주형 데이터이므로 신경망에 숫자를 그대로 넣지 않고 One-Hot Encoding을 적용한다.
encoder = Pipeline(
steps=[
("imputer", SimpleImputer(
strategy="constant",
fill_value=-1
)),
("onehot", OneHotEncoder(
handle_unknown="ignore"
))
]
)
인코더는 훈련 데이터로만 학습해야 한다.
X_train_encoded = encoder.fit_transform(X_train)
X_val_encoded = encoder.transform(X_val)
X_test_encoded = encoder.transform(X_test)
- fit_transform()은 train 데이터에만 사용
- validation/test에는 transform()만 사용
희소 행렬을 Dense 배열로 변환
OneHotEncoder 결과는 전체 값 중 대부분이 0으로 채워진 희소 행렬(sparse matrix)이다. MLP에 넣기 위해 float32 형태의 NumPy 배열로 변환해야 한다.
def to_dense_float32(x):
if hasattr(x, "toarray"):
return x.toarray().astype(np.float32)
return np.asarray(x, dtype=np.float32)
X_train_np = to_dense_float32(X_train_encoded)
X_val_np = to_dense_float32(X_val_encoded)
X_test_np = to_dense_float32(X_test_encoded)
y_train_np = y_train.to_numpy(dtype=np.float32).reshape(-1, 1)
y_val_np = y_val.to_numpy(dtype=np.float32).reshape(-1, 1)
y_test_np = y_test.to_numpy(dtype=np.float32).reshape(-1, 1)
print("인코딩 후 입력 특성 개수:", X_train_np.shape[1]) # 인코딩 후 입력 특성 개수: 2951
NumPy 배열을 PyTorch Tensor로 변환
X_train_tensor = torch.tensor(X_train_np, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train_np, dtype=torch.float32)
X_val_tensor = torch.tensor(X_val_np, dtype=torch.float32)
y_val_tensor = torch.tensor(y_val_np, dtype=torch.float32)
X_test_tensor = torch.tensor(X_test_np, dtype=torch.float32)
y_test_tensor = torch.tensor(y_test_np, dtype=torch.float32)
TensorDataset과 DataLoader 만들기
# TensorDataset 만들기
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
# DataLoader 만들기
BATCH_SIZE = 256
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True
)
val_loader = DataLoader(
val_dataset,
batch_size=BATCH_SIZE,
shuffle=False
)
test_loader = DataLoader(
test_dataset,
batch_size=BATCH_SIZE,
shuffle=False
)
TensorDataset: 입력 데이터와 정답 데이터를 하나로 묶음DataLoader: 데이터를 작은 batch 단위로 나눠서 모델에 전달함train_loader:shuffle=True→ 매 epoch마다 데이터 순서를 섞음val_loader/test_loader:shuffle=False→ 평가 시에는 순서를 섞을 필요 없음
MLP 모델 정의

class MLP(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_dim, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, 1)
)
def forward(self, x):
return self.network(x)
ReLU: 신경망에 비선형성을 추가해 단순 선형 모델보다 복잡한 패턴을 학습 가능하게 하는 역할Dropout: 학습 중 일부 값을 무작위로 0으로 만들어 과적합을 줄이는 역할- 손실 함수로 내부적으로 Sigmoid가 포함된
nn.BCEWithLogitLoss()를 사용할 것이므로Sigmoid()를 출력층에 넣지 않음
Device, 모델, 손실 함수, Optimizer 설정
# GPU 또는 CPU 사용
device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
print("사용 장치:", device) # 사용 장치: cpu
# 모델 생성
input_dim = X_train_np.shape[1]
model = MLP(input_dim=input_dim).to(device)
print(model)
'''
MLP(
(network): Sequential(
(0): Linear(in_features=2951, out_features=256, bias=True)
(1): ReLU()
(2): Dropout(p=0.3, inplace=False)
(3): Linear(in_features=256, out_features=64, bias=True)
(4): ReLU()
(5): Dropout(p=0.2, inplace=False)
(6): Linear(in_features=64, out_features=1, bias=True)
)
)
'''
# 손실 함수 및 optimizer 정의
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.AdamW(
model.parameters(),
lr=0.001,
weight_decay=1e-4
)
criterion: 모델 예측과 실제 정답의 차이를 계산optimizer: 손실이 줄어들도록 모델 가중치를 수정lr: learning rate, 한 번 업데이트할 때 이동하는 크기weight_decay: 과적합 완화에 도움을 줄 수 있는 규제 항목
평가 함수 만들기
@torch.no_grad() # 미분 자동 계산 OFF
def evaluate(model, data_loader, criterion, device):
model.eval() # Dropout 비활성화
total_loss = 0
all_probs = []
all_targets = []
for X_batch, y_batch in data_loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
logits = model(X_batch)
loss = criterion(logits, y_batch)
total_loss += loss.item() * X_batch.size(0)
probs = torch.sigmoid(logits)
all_probs.extend(probs.cpu().numpy().ravel())
all_targets.extend(y_batch.cpu().numpy().ravel())
all_probs = np.array(all_probs)
all_targets = np.array(all_targets)
predictions = (all_probs >= 0.5).astype(int)
avg_loss = total_loss / len(data_loader.dataset)
accuracy = accuracy_score(all_targets, predictions)
auc = roc_auc_score(all_targets, all_probs)
return avg_loss, accuracy, auc
학습 루프 만들기
# 학습 루프 만들기
'''
1. 배치 데이터 가져오기
2. 모델 예측
3. 손실 계산
4. 기존 gradient 초기화
5. 역전파(backpropagation)
6. optimizer로 가중치 업데이트
'''
EPOCHS = 50
PATIENCE = 5
best_val_auc = -np.inf
best_model_state = None
patience_count = 0
history = {
"train_loss": [],
"val_loss": [],
"val_accuracy": [],
"val_auc": []
}
for epoch in range(EPOCHS):
model.train()
total_train_loss = 0
for X_batch, y_batch in train_loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
# 1. 예측
logits = model(X_batch)
# 2. 손실 계산
loss = criterion(logits, y_batch)
# 3. 이전 gradient 초기화
optimizer.zero_grad()
# 4. 역전파
loss.backward()
# 5. 모델 파라미터 업데이트
optimizer.step()
total_train_loss += loss.item() * X_batch.size(0)
# Epoch별 평균 train loss
train_loss = total_train_loss / len(train_loader.dataset)
# Validation 평가
val_loss, val_accuracy, val_auc = evaluate(
model=model,
data_loader=val_loader,
criterion=criterion,
device=device
)
history["train_loss"].append(train_loss)
history["val_loss"].append(val_loss)
history["val_accuracy"].append(val_accuracy)
history["val_auc"].append(val_auc)
print(
f"Epoch [{epoch + 1:02d}/{EPOCHS}] | "
f"Train Loss: {train_loss:.4f} | "
f"Val Loss: {val_loss:.4f} | "
f"Val Accuracy: {val_accuracy:.4f} | "
f"Val AUC: {val_auc:.4f}"
)
# Early Stopping
if val_auc > best_val_auc:
best_val_auc = val_auc
best_model_state = copy.deepcopy(model.state_dict())
patience_count = 0
else:
patience_count += 1
if patience_count >= PATIENCE:
print(f"\nEarly Stopping: Epoch {epoch + 1}에서 학습 종료")
break
model.load_state_dict(best_model_state)
학습을 오래 하면 train 데이터에서는 성능이 계속 좋아질 수 있지만, validation 성능은 오히려 나빠질 수 있다. 이것이 과적합(Overfitting)의 대표적인 신호이며, Early Stopping은 validation 성능이 더 이상 좋아지지 않을 때 학습을 멈추는 방법이다.

학습 과정 시각화
# Loss 그래프
plt.figure(figsize=(10, 5))
plt.plot(history["train_loss"], label="Train Loss")
plt.plot(history["val_loss"], label="Validation Loss")
plt.title("Training / Validation Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.show()

- Train Loss와 Validation Loss가 함께 감소: 좋은 학습 흐름
- Train Loss만 계속 감소하고 Validation Loss가 증가: 과적합 가능성
- 둘 다 거의 감소하지 않음: 학습률, 구조, 데이터 표현을 점검할 필요
# Validation ROC-AUC 그래프
plt.figure(figsize=(10, 5))
plt.plot(history["val_auc"], marker="o", label="Validation ROC-AUC")
plt.title("Validation ROC-AUC")
plt.xlabel("Epoch")
plt.ylabel("ROC-AUC")
plt.legend()
plt.show()

최종 Test 데이터 평가
test_loss, test_accuracy, test_auc = evaluate(
model=model,
data_loader=test_loader,
criterion=criterion,
device=device
)
print(f"Test Loss: {test_loss:.4f}") # Test Loss: 1.6895
print(f"Test Accuracy: {test_accuracy:.4f}") # Test Accuracy: 0.5262
print(f"Test ROC-AUC: {test_auc:.4f}") # Test ROC-AUC: 0.5296
| 모델 | Test Accuracy | Test ROC-AUC |
| Dummy Classifier | 0.5 | 0.5 |
| Logistic Regression | 0.5316 | 0.5409 |
| MLP | 0.5262 | 0.5296 |
로지스틱 회귀 결과와 비교했을 때 MLP의 결과가 높았다면 챔피언/밴/주문 사이의 비선형 관계를 일부 학습했을 가능성이 있었겠지만, 비교 결과 오히려 성능이 더 낮게 나왔다.
- 현재 데이터에는 복잡한 모델이 필요하지 않음
- 신경망 구조가 적절하지 않음
- 학습률 또는 Dropout 비율이 맞지 않음
- 입력 특성 자체에 승패 정보가 제한적임
- 과적합이 발생함
모델 수정하기
Validation Loss가 계속 증가했고 모델 성능이 선형 회귀 모델보다 나쁘게 나왔으므로 과적합이 의심된다. 선형 회귀 모델의 성능이 더 좋게 나왔으므로 현재 모델이 데이터에 비해 너무 복잡할 가능성이 있다.
class SmallMLP(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 1)
)
def forward(self, x):
return self.network(x)



기존 MLP보다 SmallMLP에서 Validation Loss의 기울기가 더 완만하게 증가하고 성능이 소폭 개선된 것을 확인할 수 있다.

이로써 다음과 같은 결론을 내릴 수 있다.
신경망이 항상 로지스틱 회귀보다 좋은 것은 아니다.
현재처럼 챔피언 ID, 밴 ID, 주문 ID를 One-Hot Encoding한 표 형태(tabular) 데이터에서는 로지스틱 회귀가 더 안정적일 수 있겠다🥲
새로운 경기 예측하기
new_game = pd.DataFrame([{
"seasonId": 9,
"t1_champ1id": 8,
"t1_champ2id": 432,
"t1_champ3id": 96,
"t1_champ4id": 11,
"t1_champ5id": 112,
"t2_champ1id": 104,
"t2_champ2id": 498,
"t2_champ3id": 122,
"t2_champ4id": 238,
"t2_champ5id": 412,
"t1_champ1_sum1": 12,
"t1_champ1_sum2": 4,
"t1_champ2_sum1": 3,
"t1_champ2_sum2": 4,
"t1_champ3_sum1": 4,
"t1_champ3_sum2": 7,
"t1_champ4_sum1": 11,
"t1_champ4_sum2": 6,
"t1_champ5_sum1": 4,
"t1_champ5_sum2": 14,
"t2_champ1_sum1": 4,
"t2_champ1_sum2": 7,
"t2_champ2_sum1": 6,
"t2_champ2_sum2": 4,
"t2_champ3_sum1": 14,
"t2_champ3_sum2": 4,
"t2_champ4_sum1": 4,
"t2_champ4_sum2": 11,
"t2_champ5_sum1": 4,
"t2_champ5_sum2": 3,
"t1_ban1": 92,
"t1_ban2": 40,
"t1_ban3": 69,
"t1_ban4": 119,
"t1_ban5": 141,
"t2_ban1": 114,
"t2_ban2": 67,
"t2_ban3": 43,
"t2_ban4": 16,
"t2_ban5": 51
}])
new_game_encoded = encoder.transform(new_game)
new_game_np = to_dense_float32(new_game_encoded)
new_game_tensor = torch.tensor(
new_game_np,
dtype=torch.float32
).to(device)
model.eval()
with torch.no_grad():
logit = model(new_game_tensor)
probability = torch.sigmoid(logit).item()
prediction = int(probability >= 0.5)
print(f"팀1 승리 확률: {probability:.2%}")
print(f"예측 결과: {'팀1 승리' if prediction == 1 else '팀2 승리'}")

🧩 로지스틱 회귀 vs MLP
| 항목 | 로지스틱 회귀 | MLP |
| 구조 | 선형 모델 | 여러 은닉층을 가진 신경망 |
| 관계 학습 | 주로 선형 관계 | 비선형, 복잡한 관계 |
| 학습 속도 | 빠름 | 상대적으로 느림 |
| 해석 | 계수 확인이 쉬움 | 상대적으로 어려움 |
| 과적합 위험 | 비교적 낮음 | 상대적으로 높음 |
| 데이터 요구량 | 비교적 적음 | 보통 더 많은 데이터가 유리 |
| 시너지 조합 효과 | 제한적 | 일부 학습 가능 |
'CS > Python' 카테고리의 다른 글
| 파이썬으로 Attention 찍먹하기 (0) | 2026.08.12 |
|---|---|
| 파이썬으로 토큰화+임베딩 찍먹하기 (0) | 2026.08.12 |
| 파이썬의 PyTorch 라이브러리에 대해 알아보기 (0) | 2026.08.10 |
| 파이썬으로 모델 검증/해석 찍먹하기 (0) | 2026.08.07 |
| 파이썬으로 머신러닝 찍먹하기 (0) | 2026.08.07 |