지식
AI
로지스틱 회귀: 분류 문제의 첫 걸음
시그모이드 함수, BCE 손실, 결정 경계, 소프트맥스 다중 분류까지 로지스틱 회귀의 원리와 scikit-learn·PyTorch 구현을 완전 정복한다.
지난 글에서 선형 회귀가 연속값 예측 문제를 풀었다. 이번에는 분류 문제를 다루는 로지스틱 회귀(Logistic Regression)를 살펴본다. 이름에 “회귀”가 붙었지만 분류 알고리즘이다. 선형 회귀의 출력을 시그모이드 함수로 변환해 확률로 만드는 것이 핵심 아이디어다.
왜 선형 회귀로 분류하면 안 될까
선형 회귀로 이진 분류(y=0 또는 1)를 시도하면 두 가지 문제가 생긴다.
- 출력 범위 문제: 선형 회귀 출력
ŷ = wx + b는 (-∞, +∞)인데, 확률은 [0, 1]이어야 한다. - 손실 함수 문제: MSE가 분류에서 비볼록(non-convex)해져 여러 지역 최솟값이 생긴다.
로지스틱 회귀는 시그모이드 함수로 이 두 문제를 동시에 해결한다.
시그모이드: 실수를 확률로
시그모이드 함수는 어떤 실수값도 (0, 1) 범위로 압축한다.
σ(z) = 1 / (1 + e⁻ᶻ)
- z → -∞ : σ → 0
- z = 0 : σ = 0.5
- z → +∞ : σ → 1
import numpy as np
import torch
import torch.nn as nn
# 시그모이드 직접 구현
def sigmoid(z):
return 1 / (1 + np.exp(-z))
z_values = np.array([-6, -3, -1, 0, 1, 3, 6])
probs = sigmoid(z_values)
for z, p in zip(z_values, probs):
bar = "█" * int(p * 20)
print(f"z={z:3d}: σ={p:.4f} {bar}")
# z=-6: σ=0.0025
# z= 0: σ=0.5000 ██████████
# z=+6: σ=0.9975 ████████████████████
# PyTorch: nn.Sigmoid() 또는 torch.sigmoid()
z_t = torch.tensor([-6.0, 0.0, 6.0])
print(torch.sigmoid(z_t)) # tensor([0.0025, 0.5000, 0.9975])
로지스틱 회귀 모델
z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b (선형 결합)
ŷ = σ(z) = P(y=1|x) (확률 출력)
결정 경계: ŷ ≥ 0.5 → class 1, ŷ < 0.5 → class 0. 이는 z ≥ 0이 class 1과 동치다.
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report
# 유방암 데이터 (이진 분류)
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 스케일링 필수 (특성 범위 차이 큼)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 로지스틱 회귀 학습
clf = LogisticRegression(C=1.0, max_iter=1000, random_state=42)
clf.fit(X_train, y_train)
# 평가
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test)[:, 1] # P(y=1) 확률
print(classification_report(y_test, y_pred))
# 결정 경계 임계값 조정 (기본 0.5)
threshold = 0.3 # 재현율 중시 → 임계값 낮춤
y_pred_adj = (y_proba >= threshold).astype(int)
print(classification_report(y_test, y_pred_adj))
손실 함수: Binary Cross-Entropy
로지스틱 회귀의 손실 함수는 MSE가 아닌 BCE(Binary Cross-Entropy)다.
L = -[y·log(ŷ) + (1-y)·log(1-ŷ)]
- y=1, ŷ→1: 손실 → 0 (정확한 예측)
- y=1, ŷ→0: 손실 → ∞ (완전히 틀린 예측)
- BCE를 최소화 = MLE (최대 우도 추정) 수행
import torch
import torch.nn as nn
import torch.optim as optim
# PyTorch로 처음부터 구현
class LogisticRegressionPyTorch(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.linear = nn.Linear(input_dim, 1) # wx + b
def forward(self, x):
return self.linear(x) # logit 반환 (sigmoid 없음)
model = LogisticRegressionPyTorch(input_dim=X_train.shape[1])
# BCEWithLogitsLoss = sigmoid + BCE (수치적으로 더 안정)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
X_t = torch.FloatTensor(X_train)
y_t = torch.FloatTensor(y_train).unsqueeze(1)
for epoch in range(200):
model.train()
optimizer.zero_grad()
logits = model(X_t)
loss = criterion(logits, y_t)
loss.backward()
optimizer.step()
# 추론: sigmoid로 확률 변환
model.eval()
with torch.no_grad():
logits = model(torch.FloatTensor(X_test))
probs = torch.sigmoid(logits).squeeze().numpy()
preds = (probs >= 0.5).astype(int)
다중 분류: Softmax
클래스가 3개 이상이면 소프트맥스(Softmax)를 사용한다.
# Softmax: P(class=k) = e^zₖ / Σⱼ e^zⱼ
# 모든 클래스 확률의 합 = 1
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# multi_class='auto' → 3클래스에서 softmax 자동 사용
clf_mc = LogisticRegression(multi_class='auto', solver='lbfgs', max_iter=500)
clf_mc.fit(X_train, y_train)
# 각 클래스의 확률
proba = clf_mc.predict_proba(X_test[:5])
print("클래스별 확률 (행 합=1):")
for row in proba:
print([f"{p:.3f}" for p in row])
# PyTorch: CrossEntropyLoss = Softmax + Log + NLL
criterion = nn.CrossEntropyLoss() # 내부에 Softmax 포함
# 마지막 레이어는 Softmax 없이 logit 출력
model_mc = nn.Linear(4, 3) # 3 클래스
logits = model_mc(torch.FloatTensor(X_train))
loss = criterion(logits, torch.LongTensor(y_train))
규제화와 클래스 불균형
# C 파라미터: 정규화 역강도 (C=1/λ)
# C 작을수록 강한 규제 → 더 단순한 모델
for C in [0.01, 0.1, 1.0, 10.0]:
clf = LogisticRegression(C=C, max_iter=500)
clf.fit(X_train, y_train)
acc = clf.score(X_test, y_test)
print(f"C={C:.2f}: 검증 정확도 = {acc:.4f}")
# 클래스 불균형 대응
clf_balanced = LogisticRegression(
class_weight='balanced', # 소수 클래스에 자동 가중치
C=1.0, max_iter=500
)
clf_balanced.fit(X_train, y_train)
# 또는 수동: class_weight={0: 1, 1: 10}
로지스틱 회귀의 장점과 한계
장점:
- 확률 출력 → 해석 용이, 불확실성 정량화
- 학습 빠름, 특성 중요도 파악 가능 (계수 크기)
- 작은 데이터에서 과적합 적음
- 딥러닝의 출력 레이어로 그대로 사용
한계:
- 결정 경계가 선형 → 비선형 문제에 약함
- 특성 간 상호작용 자동 학습 불가
- 이미지, 텍스트 같은 고차원 비정형 데이터에 직접 적용 어려움
로지스틱 회귀는 딥러닝의 마지막 레이어(분류 헤드)와 수학적으로 동일하다. 따라서 이 알고리즘을 이해하면 신경망의 출력 레이어도 자연스럽게 이해된다.
지난 글: 선형 회귀: 예측 모델의 출발점
다음 글: K-최근접 이웃(KNN): 가장 직관적인 분류 알고리즘
읽어주셔서 감사합니다. 😊