MLP(Multi-Layer Perceptron) 실습
상황
고객 데이터를 이용해 이탈 여부를 예측하고자 한다. 단순한 FNN으로는 성능이 한계에 부딪힌다고 판단될 때?
해석
고객 이탈 예측은 고객별 다양한 특성(나이, 가입 기간, 요금제, 최근 이용 여부 등)을 기반으로 이탈 가능성(0 또는 1)을 예측하는 이진 분류(binary classification) 문제이다. 이 문제에서 입력 데이터는 일반적으로 각 샘플(고객)에 대해 다차원 특성 벡터로 구성되며, 시간이나 공간의 구조는 포함되어 있지 않다. 따라서 CNN이나 RNN 계열 모델보다 일반적인 전방향 신경망 구조가 적합하다.
이전에는 단일 은닉층으로 구성된 Feedforward Neural Network(FNN)를 사용할 수 있었지만, 고객 이탈과 관련된 특성 간의 관계가 단순하지 않고 상호작용이 복잡하게 얽혀 있을 가능성이 크므로, 더 많은 표현력을 가진 Multi-Layer Perceptron(MLP) 모델을 사용하는 것이 바람직하다.
MLP는 입력층과 출력층 사이에 두 개 이상의 은닉층(hidden layers) 을 포함하는 구조로, 각 층에는 선형 변환과 함께 비선형 활성화 함수가 적용된다. 은닉층을 늘림으로써 단순한 선형 관계뿐만 아니라 복잡하고 비선형적인 특성 간의 상호작용까지 학습할 수 있으며, 이는 FNN보다 더 뛰어난 모델링 성능으로 이어진다.
모델링
예를 들어 고객 특성이 30개라고 가정할 경우, 다음과 같이 MLP 모델을 설계할 수 있다.

이 모델은 총 3개의 은닉층을 포함하며, 각 층의 출력 차원을 점차 줄여가면서 비선형 변환을 수행한다. 은닉층 사이에는 ReLU 활성화 함수와 Dropout 정규화를 적용하여 과적합을 방지하고, 모델의 일반화 성능을 높인다.
왜 Input(30) → 128 → 64 → 32 → 1 구조로 구성하는가? = 층의 크기를 어떻게 설계하느냐??
128 대신 256, 512로 늘리면 표현력은 올라가지만 과적합 가능성도 올라가고, 너무 작게만들면 패턴학습을 못한다.
표현력 확보: 30 → 128로 확장하는 이유 = 처음에는 충분한 자유도를 확보해 복잡한 표현을 만들어내도록 유도
- 입력은 30개의 원시 피처(feature)다.
하지만 이 피처들이 결합하면 훨씬 더 많은 잠재적 조합이 존재할 수 있어. - 예를 들어, 나이, 가입기간, 요금제 같은 피처가 서로 복합적인 방식으로 작용해서 "이탈 가능성"에 영향을 준다면, 단일 선형 조합으로는 이 관계를 제대로 설명하지 못해.
- 그래서 처음 은닉층의 뉴런 수를 128처럼 넉넉하게 늘려서 비선형 조합 공간을 넓혀주기 위함
- 비유하자면, 연필 30자루를 가지고 멋진 그림을 그리려면, 일단 도화지는 넓어야 하는 것과 비슷
점진적 축소: 128 → 64 → 32로 줄이는 이유
- 앞쪽에서 많은 표현을 만들어냈다면, 그걸 점차 요약해가는 과정이 필요해.
- 이건 피처 압축(feature compression) 또는 고차원 표현을 저차원으로 정제하는 과정이라고 생각하면 돼.
- 각 은닉층을 거치면서 중요한 정보는 유지하고, 덜 중요한 정보는 줄이면서 더 간결하고 의미 있는 표현으로 바꿔주는 역할을 한다.
- 비유하자면, 복잡하게 찍은 사진을 필터링하고 압축하면서, 마지막엔 핵심만 남긴다는 느낌이야.
출력층: Linear(32 → 1) → Sigmoid의 이유
- 최종 목표는 이탈 여부(0 또는 1)를 예측하는 것.
- 이진 분류 문제이므로 출력층은 하나의 뉴런만 필요해.
- 여기에 Sigmoid 함수를 적용하면 출력값이 0~1 사이의 확률 값이 돼서 "이 고객은 이탈할 확률이 몇 퍼센트다"라고 해석할 수 있다.
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 1. 가상 데이터 생성 (30개의 특성, 이진 분류)
X, y = make_classification(n_samples=1000, n_features=30,
n_informative=15, n_redundant=5,
n_classes=2, random_state=42)
# 2. 정규화 전처리
scaler = StandardScaler()
X = scaler.fit_transform(X)
# 3. 학습/검증 데이터 분할
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
# 4. 텐서 변환
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train.reshape(-1, 1), dtype=torch.float32)
X_val_tensor = torch.tensor(X_val, dtype=torch.float32)
y_val_tensor = torch.tensor(y_val.reshape(-1, 1), dtype=torch.float32)
# 5. MLP 모델 정의
class MLP(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.model = nn.Sequential(
nn.Linear(input_dim, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 1),
nn.Sigmoid() # 이진 분류 확률 출력
)
def forward(self, x):
return self.model(x)
# 6. 모델, 손실 함수, 옵티마이저 설정
model = MLP(input_dim=30)
criterion = nn.BCELoss() # Binary Cross Entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 7. 학습 루프
epochs = 20
for epoch in range(epochs):
model.train()
y_pred = model(X_train_tensor)
loss = criterion(y_pred, y_train_tensor)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 검증
model.eval()
with torch.no_grad():
val_pred = model(X_val_tensor)
val_loss = criterion(val_pred, y_val_tensor)
val_acc = ((val_pred > 0.5) == y_val_tensor).float().mean()
print(f"[Epoch {epoch+1}] Train Loss: {loss.item():.4f}, Val Loss: {val_loss.item():.4f}, Val Acc: {val_acc.item():
- ReLU (Rectified Linear Unit):
은닉층의 활성화 함수로 사용되며, 입력이 0 이하일 경우 0을 출력하고, 양수일 경우 그대로 전달한다. ReLU는 계산이 간단하면서도 기울기 소실 문제를 줄이는 데 효과적이어서 대부분의 MLP에서 기본 활성화 함수로 사용된다. - Dropout:
각 은닉층 뒤에 Dropout을 적용하여 학습 중 일부 뉴런을 무작위로 비활성화함으로써 모델이 특정 경로에 과도하게 의존하는 것을 막고, 일반화 성능을 높인다. 일반적으로 0.2~0.5 정도의 비율을 사용한다. - Sigmoid 출력층:
이진 분류 문제이므로, 출력층에는 하나의 노드를 두고 Sigmoid 활성화 함수를 적용하여 결과를 0~1 사이의 확률값으로 출력한다. - Binary Cross Entropy 손실 함수:
모델이 출력한 확률값과 실제 정답(0 또는 1) 사이의 오차를 계산하기 위해 Binary Cross Entropy(BCE) 손실 함수를 사용한다. 이 함수는 다음과 같은 수식으로 표현된다:BCE=−[y⋅log(y^)+(1−y)⋅log(1−y^)]BCE = -[y \cdot \log(\hat{y}) + (1 - y) \cdot \log(1 - \hat{y})]여기서 yy는 실제값, y^\hat{y}는 모델이 출력한 확률이다. - Adam 옵티마이저:
파라미터 업데이트 시 관성(momentum)과 적응적 학습률(adaptive learning rate)을 결합한 Adam 옵티마이저는 빠르고 안정적인 수렴을 유도하여 MLP 학습에 적합하다.
입력데이터 전처리
- MLP는 입력값으로 고정 크기의 숫자 벡터를 받기 때문에, 범주형 변수는 원-핫 인코딩 혹은 임베딩 벡터로 변환하고, 수치형 변수는 정규화(StandardScaler 또는 MinMaxScaler 등)를 적용하는 것이 바람직하다. 전처리를 적절히 수행하지 않으면 학습이 불안정해질 수 있으므로 필수적인 단계이다.
결론 요약
고객 이탈 예측 문제에서 단순한 FNN으로는 데이터 내 복잡한 관계를 충분히 표현하기 어렵기 때문에, 은닉층을 여러 개 두고 ReLU와 Dropout을 활용하는 MLP 구조가 더 적합하다. 출력층은 Sigmoid 함수를 통해 이진 분류 확률을 출력하며, 손실 함수로는 Binary Cross Entropy, 옵티마이저로는 Adam을 사용하는 것이 일반적이다. 이와 같은 구성은 고객 특성 간의 비선형 관계를 효과적으로 학습하고, 과적합 없이 일반화된 예측 성능을 얻는 데에 유리하다.