k-최근접 이웃(kNN)과 나이브 베이즈
k-최근접 이웃(kNN)과 나이브 베이즈¶
개요¶
k-최근접 이웃(kNN)은 거리 기반 분류 알고리즘이고, 나이브 베이즈는 확률 기반 분류 알고리즘입니다. 두 알고리즘 모두 간단하면서도 효과적인 분류기입니다.
1. k-최근접 이웃 (k-Nearest Neighbors)¶
1.1 kNN의 기본 개념¶
"""
kNN 알고리즘:
1. 새로운 데이터 포인트가 들어오면
2. 학습 데이터에서 가장 가까운 k개의 이웃을 찾음
3. k개 이웃의 다수결(분류) 또는 평균(회귀)으로 예측
특징:
- 게으른 학습 (Lazy Learning): 학습 시 모델 생성 안함
- 인스턴스 기반 학습: 모든 학습 데이터 저장
- 비모수적 방법: 데이터 분포 가정 없음
"""
import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.datasets import load_iris, load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
1.2 kNN 시각화¶
from sklearn.datasets import make_classification
# 2D 데이터 생성
X, y = make_classification(
n_samples=100, n_features=2, n_redundant=0,
n_informative=2, n_clusters_per_class=1, random_state=42
)
# kNN 시각화
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
k_values = [1, 5, 15]
for ax, k in zip(axes, k_values):
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X, y)
# 결정 경계
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
ax.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='black')
ax.set_title(f'k = {k}\nAccuracy = {knn.score(X, y):.3f}')
plt.tight_layout()
plt.show()
1.3 기본 사용법¶
# 데이터 로드
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.2, random_state=42
)
# kNN 분류기
knn = KNeighborsClassifier(
n_neighbors=5, # k값
weights='uniform', # 가중치: 'uniform' 또는 'distance'
algorithm='auto', # 알고리즘: 'auto', 'ball_tree', 'kd_tree', 'brute'
metric='minkowski', # 거리 측정: 'euclidean', 'manhattan', 'minkowski'
p=2 # minkowski p값 (2=euclidean, 1=manhattan)
)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
print("kNN 분류 결과:")
print(f" 정확도: {accuracy_score(y_test, y_pred):.4f}")
print("\n분류 리포트:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))
2. 거리 측정 방법¶
2.1 주요 거리 메트릭¶
from scipy.spatial.distance import euclidean, cityblock, minkowski, chebyshev
"""
거리 측정 방법:
1. 유클리드 거리 (Euclidean, L2):
d = sqrt(Σ(x_i - y_i)²)
- 가장 일반적으로 사용
2. 맨해튼 거리 (Manhattan, L1):
d = Σ|x_i - y_i|
- 직각 좌표계에서의 거리
- 고차원에서 더 효과적일 수 있음
3. 민코프스키 거리 (Minkowski):
d = (Σ|x_i - y_i|^p)^(1/p)
- p=1: 맨해튼, p=2: 유클리드
4. 체비셰프 거리 (Chebyshev, L∞):
d = max(|x_i - y_i|)
- 모든 차원에서 최대 차이
"""
# 예시
point1 = np.array([1, 2, 3])
point2 = np.array([4, 5, 6])
print("거리 측정 예시:")
print(f" 유클리드: {euclidean(point1, point2):.4f}")
print(f" 맨해튼: {cityblock(point1, point2):.4f}")
print(f" 민코프스키 (p=3): {minkowski(point1, point2, p=3):.4f}")
print(f" 체비셰프: {chebyshev(point1, point2):.4f}")
2.2 거리 메트릭 비교¶
# 거리 메트릭별 성능 비교
metrics = ['euclidean', 'manhattan', 'chebyshev']
print("거리 메트릭별 성능:")
for metric in metrics:
knn = KNeighborsClassifier(n_neighbors=5, metric=metric)
knn.fit(X_train, y_train)
acc = knn.score(X_test, y_test)
print(f" {metric}: {acc:.4f}")
3. k값 선택¶
3.1 k값에 따른 성능 변화¶
# k값에 따른 성능 변화
k_range = range(1, 31)
train_scores = []
test_scores = []
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)
train_scores.append(knn.score(X_train, y_train))
test_scores.append(knn.score(X_test, y_test))
# 시각화
plt.figure(figsize=(10, 6))
plt.plot(k_range, train_scores, 'o-', label='Train')
plt.plot(k_range, test_scores, 's-', label='Test')
plt.xlabel('k (Number of Neighbors)')
plt.ylabel('Accuracy')
plt.title('kNN: k vs Accuracy')
plt.legend()
plt.grid(True, alpha=0.3)
plt.xticks(k_range[::2])
plt.show()
# 최적 k 찾기
best_k = k_range[np.argmax(test_scores)]
print(f"최적 k: {best_k}")
print(f"최고 테스트 정확도: {max(test_scores):.4f}")
3.2 교차 검증으로 k 선택¶
from sklearn.model_selection import cross_val_score
# 교차 검증으로 k 선택
k_range = range(1, 31)
cv_scores = []
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X_train, y_train, cv=5, scoring='accuracy')
cv_scores.append(scores.mean())
# 시각화
plt.figure(figsize=(10, 6))
plt.plot(k_range, cv_scores, 'o-')
plt.xlabel('k')
plt.ylabel('Cross-Validation Accuracy')
plt.title('kNN: k Selection with Cross-Validation')
plt.grid(True, alpha=0.3)
plt.xticks(k_range[::2])
plt.show()
best_k = k_range[np.argmax(cv_scores)]
print(f"교차 검증 최적 k: {best_k}")
print(f"최고 CV 정확도: {max(cv_scores):.4f}")
4. 가중 kNN¶
"""
가중 kNN (Weighted kNN):
1. uniform: 모든 이웃에 동일한 가중치
- 다수결 투표
2. distance: 거리에 반비례하는 가중치
- 가까운 이웃에 더 큰 가중치
- weight = 1 / distance
"""
# 가중치 비교
weights = ['uniform', 'distance']
print("가중치 방식 비교:")
for weight in weights:
knn = KNeighborsClassifier(n_neighbors=5, weights=weight)
knn.fit(X_train, y_train)
acc = knn.score(X_test, y_test)
print(f" {weight}: {acc:.4f}")
# 거리 가중 kNN 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for ax, weight in zip(axes, weights):
knn = KNeighborsClassifier(n_neighbors=15, weights=weight)
knn.fit(X[:, :2], y)
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
ax.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='black')
ax.set_title(f'weights = {weight}')
plt.tight_layout()
plt.show()
5. kNN 회귀¶
from sklearn.neighbors import KNeighborsRegressor
from sklearn.datasets import load_diabetes
from sklearn.metrics import mean_squared_error, r2_score
# 데이터 로드
diabetes = load_diabetes()
X_train, X_test, y_train, y_test = train_test_split(
diabetes.data, diabetes.target, test_size=0.2, random_state=42
)
# 스케일링 (kNN은 거리 기반이므로 필수)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# kNN 회귀
knn_reg = KNeighborsRegressor(n_neighbors=5, weights='distance')
knn_reg.fit(X_train_scaled, y_train)
y_pred = knn_reg.predict(X_test_scaled)
print("kNN 회귀 결과:")
print(f" MSE: {mean_squared_error(y_test, y_pred):.4f}")
print(f" RMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.4f}")
print(f" R²: {r2_score(y_test, y_pred):.4f}")
6. kNN의 장단점과 최적화¶
"""
장점:
1. 간단하고 직관적
2. 학습 시간 없음 (게으른 학습)
3. 비모수적: 데이터 분포 가정 불필요
4. 다중 클래스 자연스럽게 처리
단점:
1. 예측 시 느림: O(n*d)
2. 메모리 많이 사용: 모든 데이터 저장
3. 차원의 저주: 고차원에서 성능 저하
4. 스케일링 필수
5. 최적 k 선택 필요
최적화 방법:
1. KD-Tree, Ball-Tree 사용
2. 차원 축소 (PCA 등)
3. 특성 선택
"""
# 알고리즘 비교
from time import time
algorithms = ['brute', 'kd_tree', 'ball_tree']
print("알고리즘별 시간 비교:")
for algo in algorithms:
knn = KNeighborsClassifier(n_neighbors=5, algorithm=algo)
# 학습 시간
start = time()
knn.fit(X_train, y_train)
fit_time = time() - start
# 예측 시간
start = time()
knn.predict(X_test)
pred_time = time() - start
print(f" {algo}: fit={fit_time:.4f}s, predict={pred_time:.4f}s")
7. 나이브 베이즈 (Naive Bayes)¶
7.1 베이즈 정리¶
"""
베이즈 정리:
P(y|X) = P(X|y) * P(y) / P(X)
- P(y|X): 사후 확률 (posterior) - 특성이 주어졌을 때 클래스 확률
- P(X|y): 우도 (likelihood) - 클래스가 주어졌을 때 특성 확률
- P(y): 사전 확률 (prior) - 클래스의 기본 확률
- P(X): 증거 (evidence) - 특성의 확률
나이브 가정 (Naive Assumption):
- 모든 특성이 서로 독립적이라고 가정
- P(X|y) = P(x1|y) * P(x2|y) * ... * P(xn|y)
분류:
y_pred = argmax_y P(y) * Π P(xi|y)
"""
from sklearn.naive_bayes import GaussianNB, MultinomialNB, BernoulliNB
7.2 가우시안 나이브 베이즈¶
"""
가우시안 나이브 베이즈:
- 연속형 특성에 사용
- 각 특성이 가우시안(정규) 분포를 따른다고 가정
- P(xi|y) = N(xi; μ_y, σ_y)
"""
# 가우시안 NB
gnb = GaussianNB()
gnb.fit(X_train, y_train)
y_pred = gnb.predict(X_test)
print("가우시안 나이브 베이즈 결과:")
print(f" 정확도: {accuracy_score(y_test, y_pred):.4f}")
# 학습된 파라미터 확인
print(f"\n클래스 사전 확률: {gnb.class_prior_}")
print(f"클래스별 평균 (처음 2개 특성):\n{gnb.theta_[:, :2]}")
print(f"클래스별 분산 (처음 2개 특성):\n{gnb.var_[:, :2]}")
7.3 확률 예측¶
# 확률 예측
y_proba = gnb.predict_proba(X_test[:5])
print("확률 예측 (처음 5개):")
print(f"클래스: {gnb.classes_}")
print(y_proba)
print(f"\n예측 클래스: {gnb.predict(X_test[:5])}")
print(f"실제 클래스: {y_test[:5]}")
7.4 다항 나이브 베이즈 (텍스트 분류)¶
"""
다항 나이브 베이즈:
- 이산형/카운트 특성에 사용
- 텍스트 분류에 주로 사용 (단어 빈도)
- P(xi|y) = (N_yi + α) / (N_y + αn)
- N_yi: 클래스 y에서 특성 i의 카운트
- α: smoothing 파라미터 (Laplace smoothing)
"""
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
# 뉴스 데이터 로드 (간단한 예시)
categories = ['sci.space', 'rec.sport.baseball', 'talk.politics.misc']
newsgroups = fetch_20newsgroups(
subset='train',
categories=categories,
remove=('headers', 'footers', 'quotes')
)
# 텍스트 벡터화
vectorizer = CountVectorizer(max_features=5000, stop_words='english')
X_news = vectorizer.fit_transform(newsgroups.data)
y_news = newsgroups.target
# 학습/테스트 분할
X_train_news, X_test_news, y_train_news, y_test_news = train_test_split(
X_news, y_news, test_size=0.2, random_state=42
)
# 다항 나이브 베이즈
mnb = MultinomialNB(alpha=1.0) # alpha: Laplace smoothing
mnb.fit(X_train_news, y_train_news)
print("다항 나이브 베이즈 (텍스트 분류) 결과:")
print(f" 정확도: {mnb.score(X_test_news, y_test_news):.4f}")
# 각 클래스의 가장 중요한 단어
feature_names = vectorizer.get_feature_names_out()
print("\n각 클래스별 상위 5개 단어:")
for i, category in enumerate(categories):
top_indices = mnb.feature_log_prob_[i].argsort()[-5:][::-1]
top_words = [feature_names[idx] for idx in top_indices]
print(f" {category}: {', '.join(top_words)}")
7.5 베르누이 나이브 베이즈¶
"""
베르누이 나이브 베이즈:
- 이진 특성에 사용
- 특성의 존재 여부 (0/1)
- 텍스트에서 단어 존재 여부로 사용
"""
# 이진 벡터화
binary_vectorizer = CountVectorizer(max_features=5000, binary=True, stop_words='english')
X_binary = binary_vectorizer.fit_transform(newsgroups.data)
X_train_bin, X_test_bin, y_train_bin, y_test_bin = train_test_split(
X_binary, y_news, test_size=0.2, random_state=42
)
# 베르누이 나이브 베이즈
bnb = BernoulliNB(alpha=1.0)
bnb.fit(X_train_bin, y_train_bin)
print("베르누이 나이브 베이즈 결과:")
print(f" 정확도: {bnb.score(X_test_bin, y_test_bin):.4f}")
8. 나이브 베이즈 비교¶
from sklearn.datasets import load_digits
# 숫자 이미지 데이터
digits = load_digits()
X_train, X_test, y_train, y_test = train_test_split(
digits.data, digits.target, test_size=0.2, random_state=42
)
# 세 가지 나이브 베이즈 비교
models = {
'Gaussian NB': GaussianNB(),
'Multinomial NB': MultinomialNB(),
'Bernoulli NB': BernoulliNB()
}
print("나이브 베이즈 모델 비교:")
for name, model in models.items():
model.fit(X_train, y_train)
acc = model.score(X_test, y_test)
print(f" {name}: {acc:.4f}")
9. 나이브 베이즈의 장단점¶
"""
장점:
1. 매우 빠름: 학습 O(n*d), 예측 O(d)
2. 적은 데이터로도 잘 작동
3. 고차원 데이터에 효과적
4. 확률 출력 제공
5. 온라인 학습 가능 (partial_fit)
단점:
1. 나이브 가정: 특성 독립성 가정이 현실에서 위반
2. 상관관계 있는 특성에 약함
3. 연속형 특성: 가우시안 가정이 항상 맞지 않음
4. Zero frequency 문제: smoothing 필요
언제 사용:
- 텍스트 분류 (스팸 필터, 감성 분석)
- 고차원, 적은 데이터
- 빠른 학습/예측 필요시
- 기준선(baseline) 모델
"""
10. 온라인 학습 (Incremental Learning)¶
# 온라인 학습 (partial_fit)
gnb = GaussianNB()
# 배치 학습 시뮬레이션
batch_size = 50
n_batches = len(X_train) // batch_size
for i in range(n_batches):
start = i * batch_size
end = start + batch_size
X_batch = X_train[start:end]
y_batch = y_train[start:end]
# 첫 배치에서 클래스 정의
if i == 0:
gnb.partial_fit(X_batch, y_batch, classes=np.unique(y_train))
else:
gnb.partial_fit(X_batch, y_batch)
print("온라인 학습 결과:")
print(f" 정확도: {gnb.score(X_test, y_test):.4f}")
11. kNN vs 나이브 베이즈 비교¶
from sklearn.datasets import load_breast_cancer
# 데이터
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, test_size=0.2, random_state=42
)
# 스케일링
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 모델 비교
models = {
'kNN (k=5)': KNeighborsClassifier(n_neighbors=5),
'kNN (weighted)': KNeighborsClassifier(n_neighbors=5, weights='distance'),
'Gaussian NB': GaussianNB()
}
print("kNN vs 나이브 베이즈 비교:")
print("-" * 50)
for name, model in models.items():
if 'kNN' in name:
model.fit(X_train_scaled, y_train)
acc = model.score(X_test_scaled, y_test)
else:
model.fit(X_train, y_train)
acc = model.score(X_test, y_test)
print(f" {name}: {acc:.4f}")
연습 문제¶
문제 1: 최적 k 찾기¶
교차 검증으로 Iris 데이터에 최적인 k를 찾으세요.
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
iris = load_iris()
X, y = iris.data, iris.target
# 풀이
k_range = range(1, 21)
cv_scores = []
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X, y, cv=5)
cv_scores.append(scores.mean())
best_k = k_range[np.argmax(cv_scores)]
print(f"최적 k: {best_k}")
print(f"최고 CV 정확도: {max(cv_scores):.4f}")
문제 2: 나이브 베이즈 텍스트 분류¶
간단한 텍스트 분류를 구현하세요.
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
# 간단한 텍스트 데이터
texts = [
"I love this movie", "Great film", "Excellent acting",
"Terrible movie", "Bad film", "Worst movie ever"
]
labels = [1, 1, 1, 0, 0, 0] # 1: positive, 0: negative
# 풀이
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(texts)
mnb = MultinomialNB()
mnb.fit(X, labels)
# 새로운 텍스트 분류
new_texts = ["This is a great movie", "I hate this film"]
X_new = vectorizer.transform(new_texts)
predictions = mnb.predict(X_new)
for text, pred in zip(new_texts, predictions):
sentiment = "Positive" if pred == 1 else "Negative"
print(f"'{text}' -> {sentiment}")
문제 3: 거리 가중 kNN¶
거리 가중치를 사용하여 kNN 회귀를 수행하세요.
from sklearn.neighbors import KNeighborsRegressor
from sklearn.datasets import load_diabetes
from sklearn.preprocessing import StandardScaler
diabetes = load_diabetes()
X_train, X_test, y_train, y_test = train_test_split(
diabetes.data, diabetes.target, test_size=0.2, random_state=42
)
# 풀이
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
knn_reg = KNeighborsRegressor(n_neighbors=5, weights='distance')
knn_reg.fit(X_train_scaled, y_train)
from sklearn.metrics import r2_score, mean_squared_error
y_pred = knn_reg.predict(X_test_scaled)
print(f"R²: {r2_score(y_test, y_pred):.4f}")
print(f"RMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.4f}")
요약¶
kNN 요약¶
| 파라미터 | 설명 | 권장 |
|---|---|---|
| n_neighbors | 이웃 수 | 교차 검증으로 선택 |
| weights | 가중치 방식 | 'distance' 추천 |
| metric | 거리 측정 | 'euclidean' 기본 |
| algorithm | 탐색 알고리즘 | 'auto' |
나이브 베이즈 요약¶
| 종류 | 특성 타입 | 용도 |
|---|---|---|
| GaussianNB | 연속형 (정규 분포) | 일반 분류 |
| MultinomialNB | 카운트/빈도 | 텍스트 분류 |
| BernoulliNB | 이진 (0/1) | 이진 특성 |
비교¶
| 특성 | kNN | 나이브 베이즈 |
|---|---|---|
| 학습 시간 | O(1) | O(n*d) |
| 예측 시간 | O(n*d) | O(d) |
| 메모리 | 높음 | 낮음 |
| 스케일링 | 필수 | 불필요 |
| 고차원 | 약함 | 강함 |
| 해석성 | 직관적 | 확률 기반 |