Post

[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_Framework

거리 계산 방식

KNN의 핵심은 어떤 거리로 이웃을 정의하느냐

거리수식특징  
유클리드 (Euclidean)$\sqrt{\Sigma(x_i - y_i)^2}$직선 거리, 기본값  
맨해튼 (Manhattan)$\Sigmax_i - y_i$격자 이동 거리, 이상치에 강함
민코프스키 (Minkowski)$(\Sigmax_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)  # 모델 학습
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()  # 그래프 출력

knn_k_search

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()

knn_roc_curve


장단점

장점

  • 구현이 단순하고 직관적
  • 학습 단계 없음 → 새로운 데이터 추가가 쉬움
  • 다중 클래스 분류에 자연스럽게 확장 가능

단점

  • 예측 시 전체 데이터와 거리 계산 → 데이터가 많을수록 느림
  • 고차원 데이터에서 거리 의미가 희미해짐 (차원의 저주)
  • 스케일링을 반드시 해야 함
  • 해석 가능성 없음
This post is licensed under CC BY 4.0 by the author.