[Python] ML-KNN(K-Nearest Neighbors, K-최근접 이웃
[Python] ML-KNN(K-Nearest Neighbors, K-최근접 이웃
KNN(K-Nearest Neighbors, K-최근접 이웃) 개념
KNN은 새로운 데이터가 주어졌을 때, 가장 가까운 K개의 이웃 데이터를 찾아 다수결(분류) 또는 평균(회귀)으로 예측하는 알고리즘
1
2
3
4
5
6
별도의 학습 과정 없음 → 데이터 자체가 모델 (Instance-Based Learning)
거리 기반 알고리즘 → 스케일링 필수
K가 작을수록 → 과적합 (복잡한 경계)
K가 클수록 → 과소적합 (부드러운 경계)
K는 홀수 권장 → 이진 분류에서 동점 방지
거리 계산 방식
KNN의 핵심은 어떤 거리로 이웃을 정의하느냐
| 거리 | 수식 | 특징 | ||
|---|---|---|---|---|
| 유클리드 (Euclidean) | $\sqrt{\Sigma(x_i - y_i)^2}$ | 직선 거리, 기본값 | ||
| 맨해튼 (Manhattan) | $\Sigma | x_i - y_i | $ | 격자 이동 거리, 이상치에 강함 |
| 민코프스키 (Minkowski) | $(\Sigma | x_i - y_i | ^p)^{1/p}$ | p=1이면 맨해튼, p=2이면 유클리드 |
차원의 저주 (Curse of Dimensionality)
KNN의 가장 큰 약점. 변수(차원)가 많아질수록 모든 데이터 포인트 간의 거리가 비슷해져 “가까운 이웃”의 의미가 희미해짐
1
2
3
4
5
6
7
2차원 : 거리 분포가 다양 → 이웃 구분 명확
100차원 : 모든 점이 비슷한 거리에 위치 → 이웃 구분 불가능
해결 방법:
→ PCA, t-SNE 등으로 차원 축소 후 KNN 적용
→ Feature Selection으로 중요 변수만 사용
→ 변수가 많으면 Tree 기반 알고리즘 고려
언제 사용하는가
사용해야 할 때
| 상황 | 이유 |
|---|---|
| 데이터 수가 적고 (수천 건 이하) 차원이 낮을 때 | 예측 시 전체 거리 계산 부담 없고 거리 의미 유효 |
| 빠른 프로토타이핑 / 베이스라인 | 구현이 가장 단순한 알고리즘 중 하나 |
| 데이터 분포가 지역적으로 유사할 때 | 비슷한 입력 → 비슷한 출력 가정이 성립 |
| 추천 시스템 초기 모델 | 사용자-아이템 간 유사도 계산에 직관적 |
사용하지 말아야 할 때
| 상황 | 이유 |
|---|---|
| 데이터가 수만 건 이상일 때 | 예측마다 전체 데이터와 거리 계산 → 매우 느림 |
| 변수가 많을 때 (20개 이상) | 차원의 저주로 거리 의미가 사라짐 |
| 실시간 예측이 필요할 때 | 예측 시마다 계산 → 응답 속도 느림 |
| 해석 가능성이 필요할 때 | 예측 근거 설명 어려움 |
실무 활용 사례
1
2
3
추천 시스템 : "이 상품을 산 사람들이 함께 산 상품"
이상 탐지 : 정상 패턴과 거리가 먼 데이터 포인트 탐지
의료 영상 : 유사한 증례 검색
Parameters
| Parameter | 설명 | Default | | ————- | ————————————– | ———– | | n_neighbors | 참조할 이웃 수 (K) | 5 | | weights | 거리 가중치 | uniform | | algorithm | 이웃 탐색 알고리즘 | auto | | metric | 거리 계산 방식 | minkowski | | p | 민코프스키 차수 (1 = 맨해튼, 2 = 유클리드) | 2 | | n_jobs | 병렬 처리 CPU 코어 수 (-1: 전체) | None |
n_neighbors: 참조할 이웃 수 K — 가장 중요한 파라미터- 값 변화별 효과
- 클수록 → 더 많은 이웃 참조 → 경계 부드러움 → 과소적합 ↑
- 작을수록 → 이웃 적음 → 경계 복잡 → 과적합 ↑
- K=1은 최근접 이웃만 참조 → 과적합 위험 가장 높음
- 최적 K는 교차 검증으로 결정 (보통 5~20)
- 값 변화별 효과
weights: 이웃의 투표 방식- 값 변화별 효과
uniform→ 모든 이웃 동일 가중치 (기본값)distance→ 가까운 이웃에 높은 가중치 ($w = 1/d$) → 일반적으로 더 좋은 성능
- 값 변화별 효과
algorithm: 이웃 탐색 내부 알고리즘auto→ 입력 데이터에 따라 자동 선택 (기본값)ball_tree→ 고차원 데이터에서 효율적kd_tree→ 저차원(~ 20개 이하) 데이터에서 빠름brute→ 전수 탐색, 소규모 데이터에 적합
metric/p: 거리 측정 방식- 값 변화별 효과
p = 2(유클리드) → 연속형 변수에 일반적p = 1(맨해튼) → 이상치에 강건, 격자형 데이터에 적합
- 값 변화별 효과
KNN 실습 — Titanic 생존자 예측
I. Library & Data Load
1
2
3
4
5
6
7
8
9
10
11
12
import warnings
warnings.filterwarnings(action = 'ignore') # 경고 메시지 숨김
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, confusion_matrix
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'AppleGothic'
1
2
titanic = pd.read_csv('./Data/Titanic.csv') # 데이터 로드
titanic
1
2
3
4
5
6
7
8
9
10
11
12
13
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
... ... ... ... ... ... ... ... ... ... ... ...
886 0 2 Montvila, Rev. Juozas male 27.0 0 0 211536 13.0000 NaN S
887 1 1 Graham, Miss. Margaret Edith female 19.0 0 0 112053 30.0000 B42 S
888 0 3 Johnston, Miss. Catherine Helen "Carrie" female NaN 1 2 W./C. 6607 23.4500 NaN S
889 1 1 Behr, Mr. Karl Howell male 26.0 0 0 111369 30.0000 C148 C
890 0 3 Dooley, Mr. Patrick male 32.0 0 0 370376 7.7500 NaN Q
891 rows × 11 columns
II. Preprocessing
II-I. Feature Engineering
1
2
3
4
5
6
7
8
9
10
11
titanic['FamSize'] = titanic['SibSp'] + titanic['Parch'] # 가족 수 파생변수 생성
use_cols = ['Survived', 'Pclass', 'Sex', 'Age', 'FamSize', 'Fare', 'Embarked']
titanic = titanic[use_cols].dropna(subset = ['Age']) # 결측값 제거
titanic[['Survived', 'Pclass', 'Sex', 'Embarked']] = \
titanic[['Survived', 'Pclass', 'Sex', 'Embarked']].astype('category') # 범주형으로 변환
titanic['Age'] = titanic['Age'].astype('int') # 정수형으로 변환
titanic = pd.get_dummies(titanic, columns = ['Pclass', 'Sex', 'Embarked'], drop_first = True) # 원-핫 인코딩
titanic
1
2
3
4
5
6
7
8
9
10
11
12
13
Survived Age FamSize Fare Pclass_2 Pclass_3 Sex_male Embarked_Q Embarked_S
0 0 22 1 7.2500 False True True False True
1 1 38 1 71.2833 False False False False False
2 1 26 0 7.9250 False True False False True
3 1 35 1 53.1000 False False False False True
4 0 35 0 8.0500 False True True False True
... ... ... ... ... ... ... ... ... ...
885 0 39 5 29.1250 False True False True False
886 0 27 0 13.0000 True False True False True
887 1 19 0 30.0000 False False False False True
889 1 26 0 30.0000 False False True False False
890 0 32 0 7.7500 False True True True False
714 rows × 9 columns
II-II. Train & Test Split
1
2
3
4
5
6
7
8
y = titanic['Survived']
X = titanic.drop(['Survived'], axis = 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 0) # 훈련/테스트 분할 (75:25)
# KNN은 스케일링 필수
scaler = StandardScaler() # 스케일러 초기화
X_train = scaler.fit_transform(X_train) # 훈련 데이터: fit 후 transform
X_test = scaler.transform(X_test) # 테스트 데이터: transform만 (fit 금지)
III. Model Train
1
2
3
4
5
6
7
8
9
10
11
KNN = KNeighborsClassifier(
n_neighbors = 5, # 분류에 사용할 이웃의 개수(k)
weights = 'uniform', # 이웃의 가중치
algorithm = 'auto', # 이웃을 검색
leaf_size = 30, # 리프 노드의 크기
p = 2, # 거리 측정 방법
metric = 'minkowski', # 거리의 평가 지표
n_jobs = -1, # 사용할 CPU 개수
)
KNN.fit(X_train, y_train) # 모델 학습
IV. Best K Search
1
2
3
4
5
6
7
8
9
10
11
12
13
test_scores = []
k_range = range(1, 30)
for k in k_range:
knn = KNeighborsClassifier(n_neighbors = k)
knn.fit(X_train, y_train) # 모델 학습
test_scores.append(knn.score(X_test, y_test))
plt.plot(k_range, test_scores)
plt.xlabel('K (n_neighbors)')
plt.ylabel('Accuracy')
plt.title('K에 따른 성능 변화')
plt.show() # 그래프 출력
V. Evaluation Score
1
2
3
4
5
6
7
8
9
pred = KNN.predict(X_test) # 클래스 예측
cfx = confusion_matrix(y_test, pred) # 혼동 행렬 계산
# [0,0]=TN, [0,1]=FP, [1,0]=FN, [1,1]=TP
sensitivity = cfx[1, 1] / (cfx[1, 0] + cfx[1, 1]) # 민감도: TP / (TP + FN)
specificity = cfx[0, 0] / (cfx[0, 0] + cfx[0, 1]) # 특이도: TN / (TN + FP)
print(f"정확도 : {accuracy_score(y_test, pred) * 100:.2f}%") # 정확도 계산
print(f"민감도 : {sensitivity * 100:.2f}%")
print(f"특이도 : {specificity * 100:.2f}%")
1
2
3
정확도 : 76.54%
민감도 : 71.05%
특이도 : 80.58%
VI. ROC Curve
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fpr, tpr, thresholds = roc_curve(y_test, pred)
J = tpr - fpr
ix = np.argmax(J) # 가장 큰 원소의 위치(최대값의 인덱스)
best_thresh = thresholds[ix]
#plot roc and best threshold
sens, spec = tpr[ix], 1 - fpr[ix]
# plot the roc curve for the model
plt.plot([0,1], [0,1], linestyle = '--', markersize = 0.01, color = 'black') # 중간 기준 선
plt.plot(fpr, tpr, marker = '.', color = 'black', markersize = 0.01, label = "Ridge AUC = %.2f" % roc_auc_score(y_test, pred))
plt.scatter(fpr[ix], tpr[ix], marker = '+', s = 100, color = 'r',
label = f"Best threshold = {best_thresh:.3f}, \nSensitivity = {sens:.3f}, \nSpecificity = {spec:.3f}")
# axis labels
plt.title("ROC Curve")
plt.xlabel("False Positive Rate(1 - Specificity)")
plt.ylabel("True Positive Rate(Sensitivity)")
plt.legend(loc = 4)
# show the plot
plt.show()
장단점
장점
- 구현이 단순하고 직관적
- 학습 단계 없음 → 새로운 데이터 추가가 쉬움
- 다중 클래스 분류에 자연스럽게 확장 가능
단점
- 예측 시 전체 데이터와 거리 계산 → 데이터가 많을수록 느림
- 고차원 데이터에서 거리 의미가 희미해짐 (차원의 저주)
- 스케일링을 반드시 해야 함
- 해석 가능성 없음
This post is licensed under CC BY 4.0 by the author.


