분류(Classification)란?
1편에서 지도학습은 회귀와 분류로 나뉜다고 했습니다. 4편에서 회귀를 다뤘으니, 이번에는 분류를 파고듭니다.
분류는 데이터를 미리 정해진 범주(클래스)에 할당하는 문제입니다.
| 유형 | 클래스 수 | 예시 |
|---|---|---|
| 이진 분류 | 2개 | 스팸/정상, 양성/음성 |
| 다중 분류 | 3개 이상 | 붓꽃 종 분류, 숫자 인식(0~9) |
1. 로지스틱 회귀 (Logistic Regression)
이름에 "회귀"가 들어가지만, 실제로는 분류 알고리즘입니다.
핵심 아이디어
선형 회귀의 출력을 시그모이드 함수에 통과시켜 0~1 사이의 확률로 변환합니다.
$$P(y=1|x) = \sigma(w^Tx + b) = \frac{1}{1 + e^{-(w^Tx + b)}}$$
- 출력이 0.5 이상이면 클래스 1, 미만이면 클래스 0
- 결정 경계는 $$w^Tx + b = 0$$인 직선(초평면)
손실 함수: 이진 교차 엔트로피 (Binary Cross-Entropy)
$$Loss = -\frac{1}{n}\sum_{i=1}^{n}[y_i \log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i)]$$
- 정답이 1인데 1에 가까운 예측을 하면 손실이 작음
- 정답이 1인데 0에 가까운 예측을 하면 손실이 매우 큼
Python 실습
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
# 2차원 데이터 생성
X, y = make_classification(
n_samples=200, n_features=2, n_redundant=0,
n_informative=2, random_state=42, n_clusters_per_class=1
)
# 모델 학습
model = LogisticRegression()
model.fit(X, y)
print(f"가중치: {model.coef_[0]}")
print(f"편향: {model.intercept_[0]:.4f}")
print(f"정확도: {model.score(X, y):.4f}")
# 결정 경계 시각화
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
np.linspace(y_min, y_max, 200))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolors='black', s=30)
plt.title('로지스틱 회귀 결정 경계')
plt.xlabel('특성 1')
plt.ylabel('특성 2')
plt.show()
2. 서포트 벡터 머신 (SVM)
핵심 아이디어
두 클래스 사이의 마진(margin)을 최대화하는 결정 경계(초평면)를 찾습니다. 마진이 클수록 일반화 성능이 좋습니다.
- 서포트 벡터: 결정 경계에 가장 가까운 데이터 포인트들
- 하드 마진: 모든 데이터를 완벽히 분리 (이상치에 취약)
- 소프트 마진: 일부 오분류를 허용 (C 파라미터로 제어)
커널 트릭 (Kernel Trick)
선형으로 분리 불가능한 데이터를 고차원 공간으로 매핑하여 분리합니다. 실제로 고차원으로 변환하지 않고 커널 함수로 효율적으로 계산합니다.
| 커널 | 수식 | 특징 |
|---|---|---|
| 선형 | $$K(x,z) = x^Tz$$ | 선형 분리 가능할 때 |
| RBF | $$K(x,z) = e^{-\gamma|x-z|^2}$$ | 가장 많이 사용, 비선형 |
| 다항식 | $$K(x,z) = (x^Tz + c)^d$$ | 다항식 결정 경계 |
Python 실습: 커널별 비교
from sklearn.svm import SVC
from sklearn.datasets import make_moons
# 초승달 모양 데이터 (선형 분리 불가)
X, y = make_moons(n_samples=200, noise=0.2, random_state=42)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
kernels = ['linear', 'rbf', 'poly']
kernel_names = ['선형 커널', 'RBF 커널', '다항식 커널']
for ax, kernel, name in zip(axes, kernels, kernel_names):
model = SVC(kernel=kernel, gamma='auto')
model.fit(X, y)
xx, yy = np.meshgrid(
np.linspace(X[:, 0].min()-0.5, X[:, 0].max()+0.5, 200),
np.linspace(X[:, 1].min()-0.5, X[:, 1].max()+0.5, 200)
)
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
ax.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolors='black', s=30)
ax.set_title(f'{name} (정확도: {model.score(X, y):.2f})')
plt.tight_layout()
plt.show()
3. 의사결정 트리 (Decision Tree)
핵심 아이디어
데이터를 if-then 규칙으로 반복적으로 분할하여 분류합니다. 사람이 읽을 수 있는 규칙을 생성하므로 해석 가능성이 매우 높습니다.
분할 기준: 불순도 (Impurity)
지니 불순도 (Gini Impurity):
$$Gini = 1 - \sum_{i=1}^{C}p_i^2$$
정보 이득 (엔트로피 기반):
$$Entropy = -\sum_{i=1}^{C}p_i \log_2(p_i)$$
- $$p_i$$: 클래스 $$i$$의 비율
- 불순도가 가장 많이 감소하는 분할을 선택
Python 실습: 트리 시각화
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.datasets import load_iris
# 붓꽃 데이터 (2개 특성만 사용하여 시각화)
iris = load_iris()
X = iris.data[:, [2, 3]] # 꽃잎 길이, 꽃잎 너비
y = iris.target
# 깊이 제한된 트리
model = DecisionTreeClassifier(max_depth=3, random_state=42)
model.fit(X, y)
# 트리 시각화
plt.figure(figsize=(15, 8))
plot_tree(model,
feature_names=['꽃잎 길이', '꽃잎 너비'],
class_names=iris.target_names,
filled=True, rounded=True,
fontsize=10)
plt.title('의사결정 트리 (max_depth=3)')
plt.show()
# 결정 경계 시각화
xx, yy = np.meshgrid(
np.linspace(X[:, 0].min()-0.5, X[:, 0].max()+0.5, 200),
np.linspace(X[:, 1].min()-0.5, X[:, 1].max()+0.5, 200)
)
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.3, cmap='viridis')
scatter = plt.scatter(X[:, 0], X[:, 1], c=y, cmap='viridis',
edgecolors='black', s=30)
plt.xlabel('꽃잎 길이 (cm)')
plt.ylabel('꽃잎 너비 (cm)')
plt.title('의사결정 트리 결정 경계')
plt.colorbar(scatter)
plt.show()
4. k-최근접 이웃 (k-NN)
핵심 아이디어
새로운 데이터가 들어오면 가장 가까운 k개의 이웃을 찾아서 다수결 투표로 분류합니다. 별도의 학습 과정이 없는 **게으른 학습기(lazy learner)**입니다.
- k가 작으면: 결정 경계가 복잡 → 과적합 위험
- k가 크면: 결정 경계가 단순 → 과소적합 위험
거리 측정 방법
| 거리 | 수식 | 특징 |
|---|---|---|
| 유클리드 | $$\sqrt{\sum(x_i-y_i)^2}$$ | 가장 일반적 |
| 맨해튼 | $$\sum|x_i-y_i|$$ | 고차원에서 유용 |
| 민코프스키 | $$(\sum|x_i-y_i|^p)^{1/p}$$ | 일반화된 거리 |
Python 실습: k값에 따른 변화
from sklearn.neighbors import KNeighborsClassifier
X, y = make_moons(n_samples=200, noise=0.3, random_state=42)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
k_values = [1, 5, 30]
for ax, k in zip(axes, k_values):
model = KNeighborsClassifier(n_neighbors=k)
model.fit(X, y)
xx, yy = np.meshgrid(
np.linspace(X[:, 0].min()-0.5, X[:, 0].max()+0.5, 200),
np.linspace(X[:, 1].min()-0.5, X[:, 1].max()+0.5, 200)
)
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
ax.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolors='black', s=30)
ax.set_title(f'k = {k} (정확도: {model.score(X, y):.2f})')
plt.tight_layout()
plt.show()
5. 나이브 베이즈 (Naive Bayes)
핵심 아이디어
베이즈 정리를 기반으로 각 클래스의 사후 확률을 계산하고, 확률이 가장 높은 클래스로 분류합니다.
$$P(y|x) = \frac{P(x|y) \cdot P(y)}{P(x)}$$
"나이브(Naive)"인 이유: 모든 특성이 서로 독립이라고 가정합니다. 현실에서는 거의 성립하지 않지만, 놀랍게도 실전에서 잘 작동합니다.
변형
| 변형 | 데이터 분포 가정 | 적합한 데이터 |
|---|---|---|
| 가우시안 | 정규분포 | 연속형 수치 데이터 |
| 다항 | 다항분포 | 텍스트 (단어 빈도) |
| 베르누이 | 이진분포 | 이진 특성 (있다/없다) |
Python 실습
from sklearn.naive_bayes import GaussianNB
X, y = make_classification(
n_samples=200, n_features=2, n_redundant=0,
n_informative=2, random_state=42
)
model = GaussianNB()
model.fit(X, y)
xx, yy = np.meshgrid(
np.linspace(X[:, 0].min()-1, X[:, 0].max()+1, 200),
np.linspace(X[:, 1].min()-1, X[:, 1].max()+1, 200)
)
Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1].reshape(xx.shape)
plt.contourf(xx, yy, Z, levels=20, alpha=0.6, cmap='RdBu')
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', edgecolors='black', s=30)
plt.colorbar(label='P(y=1)')
plt.title(f'나이브 베이즈 확률 분포 (정확도: {model.score(X, y):.2f})')
plt.show()
6. 분류 평가 지표
분류는 단순 정확도만으로 평가하면 안 됩니다. 특히 불균형 데이터에서는 더욱 그렇습니다.
혼동 행렬 (Confusion Matrix)
| 예측: 양성 | 예측: 음성 | |
|---|---|---|
| 실제: 양성 | TP (참 양성) | FN (거짓 음성) |
| 실제: 음성 | FP (거짓 양성) | TN (참 음성) |
핵심 지표
| 지표 | 수식 | 의미 |
|---|---|---|
| 정확도 | $$\frac{TP+TN}{TP+TN+FP+FN}$$ | 전체 중 맞춘 비율 |
| 정밀도 | $$\frac{TP}{TP+FP}$$ | 양성 예측 중 실제 양성 비율 |
| 재현율 | $$\frac{TP}{TP+FN}$$ | 실제 양성 중 찾아낸 비율 |
| F1-Score | $$2 \cdot \frac{정밀도 \cdot 재현율}{정밀도 + 재현율}$$ | 정밀도와 재현율의 조화 평균 |
정밀도 vs 재현율: 스팸 필터는 정밀도가 중요 (정상 메일을 스팸으로 분류하면 안 됨). 암 진단은 재현율이 중요 (암 환자를 놓치면 안 됨).
from sklearn.metrics import (confusion_matrix, classification_report,
ConfusionMatrixDisplay)
# 예시 (나중 토이 프로블럼에서 사용)
def plot_confusion_matrix(y_true, y_pred, labels=None, title="혼동 행렬"):
cm = confusion_matrix(y_true, y_pred)
disp = ConfusionMatrixDisplay(cm, display_labels=labels)
disp.plot(cmap='Blues')
plt.title(title)
plt.show()
print(classification_report(y_true, y_pred, target_names=labels))
7. 토이 프로블럼 ①: 붓꽃 품종 분류
7-1. 데이터 탐색
import pandas as pd
from sklearn.datasets import load_iris
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = pd.Series(iris.target, name='species')
print("=== 데이터 개요 ===")
print(f"샘플 수: {X.shape[0]}, 특성 수: {X.shape[1]}")
print(f"클래스: {iris.target_names} (각 50개)")
print()
print(X.describe().round(2))
# 특성 분포 시각화
import seaborn as sns
df = X.copy()
df['종'] = [iris.target_names[i] for i in y]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for ax, col in zip(axes.ravel(), iris.feature_names):
sns.boxplot(data=df, x='종', y=col, ax=ax, palette='Set2')
ax.set_title(col)
plt.tight_layout()
plt.show()
# 페어플롯 (특성 간 관계)
sns.pairplot(df, hue='종', palette='Set2', diag_kind='kde')
plt.suptitle('붓꽃 특성 페어플롯', y=1.02)
plt.show()
7-2. 전처리 및 학습
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42, stratify=iris.target
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
7-3. 5가지 알고리즘 비교
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score
models = {
'로지스틱 회귀': LogisticRegression(max_iter=200),
'SVM (RBF)': SVC(kernel='rbf', gamma='auto'),
'의사결정 트리': DecisionTreeClassifier(max_depth=4, random_state=42),
'k-NN (k=5)': KNeighborsClassifier(n_neighbors=5),
'나이브 베이즈': GaussianNB(),
}
print(f"{'모델':<16} {'Train 정확도':>12} {'Test 정확도':>12} {'CV 평균(5-fold)':>16}")
print("-" * 60)
for name, model in models.items():
model.fit(X_train_scaled, y_train)
train_acc = model.score(X_train_scaled, y_train)
test_acc = model.score(X_test_scaled, y_test)
cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=5)
print(f"{name:<16} {train_acc:>12.4f} {test_acc:>12.4f} {cv_scores.mean():>12.4f} ± {cv_scores.std():.4f}")
7-4. 최적 모델 상세 평가
# SVM이 일반적으로 가장 좋은 성능
best_model = SVC(kernel='rbf', gamma='auto')
best_model.fit(X_train_scaled, y_train)
y_pred = best_model.predict(X_test_scaled)
# 혼동 행렬
plot_confusion_matrix(y_test, y_pred, labels=iris.target_names,
title='SVM - 붓꽃 분류 혼동 행렬')
8. 토이 프로블럼 ②: 유방암 진단 (이진 분류)
실제 의료 데이터를 활용한 이진 분류 문제입니다.
8-1. 데이터 탐색
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = pd.Series(cancer.target, name='diagnosis')
print("=== 데이터 개요 ===")
print(f"샘플 수: {X.shape[0]}, 특성 수: {X.shape[1]}")
print(f"클래스: {cancer.target_names}")
print(f" 악성(0): {(y == 0).sum()}개")
print(f" 양성(1): {(y == 1).sum()}개")
print(f" 양성 비율: {y.mean():.2%}")
8-2. 전처리
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, test_size=0.2, random_state=42, stratify=cancer.target
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"학습 세트: {X_train.shape[0]}개")
print(f"테스트 세트: {X_test.shape[0]}개")
8-3. 모델 비교
from sklearn.metrics import f1_score, recall_score, precision_score
models = {
'로지스틱 회귀': LogisticRegression(max_iter=5000),
'SVM (RBF)': SVC(kernel='rbf'),
'의사결정 트리': DecisionTreeClassifier(max_depth=5, random_state=42),
'k-NN (k=5)': KNeighborsClassifier(n_neighbors=5),
'나이브 베이즈': GaussianNB(),
}
print(f"{'모델':<16} {'정확도':>8} {'정밀도':>8} {'재현율':>8} {'F1':>8}")
print("-" * 52)
for name, model in models.items():
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
acc = accuracy_score(y_test, y_pred)
prec = precision_score(y_test, y_pred)
rec = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
print(f"{name:<16} {acc:>8.4f} {prec:>8.4f} {rec:>8.4f} {f1:>8.4f}")
8-4. 최적 모델 상세 분석
# 로지스틱 회귀 - 해석 가능하고 성능도 좋음
best_model = LogisticRegression(max_iter=5000)
best_model.fit(X_train_scaled, y_train)
y_pred = best_model.predict(X_test_scaled)
# 혼동 행렬
plot_confusion_matrix(y_test, y_pred, labels=cancer.target_names,
title='로지스틱 회귀 - 유방암 진단 혼동 행렬')
의료 진단에서는 재현율이 핵심: FN(거짓 음성 = 암인데 정상 판정)이 가장 위험합니다. 재현율이 낮은 모델은 의료 현장에서 사용할 수 없습니다.
8-5. 특성 중요도 분석
# 로지스틱 회귀 가중치 분석
importance = pd.Series(
np.abs(best_model.coef_[0]),
index=cancer.feature_names
).sort_values(ascending=False).head(10)
plt.figure(figsize=(10, 6))
importance.sort_values().plot(kind='barh')
plt.title('유방암 진단: 상위 10개 중요 특성')
plt.xlabel('|가중치|')
plt.tight_layout()
plt.show()
# 상위 특성 해석
print("\n상위 5개 특성:")
for feat in importance.head(5).index:
coef = best_model.coef_[0][list(cancer.feature_names).index(feat)]
direction = "양성(정상)" if coef > 0 else "악성"
print(f" {feat}: 가중치 {coef:.4f} → 값이 클수록 {direction}일 확률 ↑")
알고리즘 선택 가이드
| 알고리즘 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|
| 로지스틱 회귀 | 빠르고 해석 쉬움, 확률 출력 | 비선형 패턴 포착 어려움 | 기본 베이스라인, 의료/금융 |
| SVM | 고차원 데이터에 강함 | 대규모 데이터에 느림 | 중소규모 + 고차원 데이터 |
| 의사결정 트리 | 직관적 규칙, 전처리 불필요 | 과적합 위험 | 해석이 필수인 경우 |
| k-NN | 단순하고 직관적 | 차원의 저주, 예측 느림 | 소규모 데이터, 프로토타이핑 |
| 나이브 베이즈 | 매우 빠름, 적은 데이터 | 독립 가정이 강함 | 텍스트 분류, 실시간 처리 |
다음 글에서는 이 분류기들을 **앙상블(조합)**하여 더 강력한 모델을 만드는 랜덤 포레스트와 부스팅을 다룹니다.