K-Nearest Neighbor (KNN)이란? 개념 이해, 예제 코드
Contentshide
기계 학습의 다양한 알고리즘 중에서도 가장 직관적이고 쉽게 이해할 수 있는 알고리즘 중 하나가 바로 K-최근접 이웃(K-Nearest Neighbor, KNN)입니다. KNN은 새로운 데이터를 분류할 때, 데이터 주변에 있는 가장 가까운 이웃 데이터를 기준으로 그 데이터가 어떤 범주에 속할지 결정하는 방식으로 동작합니다.
1. KNN의 개념과 원리
KNN의 원리를 현실의 예시를 들어 설명하면 매우 쉽게 이해할 수 있습니다. 예를 들어, 한 사람의 성격이나 특성을 알고 싶을 때, 그 사람과 친한 주변 친구들을 보면 대략적으로 알 수 있는 것과 유사합니다. 이처럼 KNN 알고리즘은 분류되지 않은 새로운 데이터가 주어지면 그 데이터로부터 가까운 'k'개의 이웃 데이터를 찾고, 가장 많이 포함된 범주로 그 데이터를 분류합니다.
쉬운 예시를 통해 이해해 봅시다.
1-1. K값에 따른 분류 사례
예를 들어, X1과 X2 두 개의 축을 기준으로 주황색 원(Class A), 초록색 원(Class B)으로 나누어진 데이터가 있고, 새로운 데이터(오렌지색 별)가 추가되었을 때:
- K=3인 경우: 가장 가까운 3개의 이웃 중에서 Class B가 2개, Class A가 1개 선택되었다면, 다수결 원칙에 따라 이 데이터는 Class B로 분류됩니다.
- K=6인 경우: 가장 가까운 6개의 이웃 중에서 Class A가 4개, Class B가 2개 선택되었다면, 다수결 원칙에 따라 이 데이터는 Class A로 분류됩니다.
이처럼 k값에 따라 분류 결과가 달라질 수 있어, 적절한 k값 설정이 중요합니다.
1-2. K값 설정의 장단점
k값 설정 | 장점 | 단점 |
---|---|---|
작은 k (예: k=1, k=3) | 빠른 결정, 간단한 계산, 지역적 패턴을 잘 반영 | 데이터의 잡음(noise)이나 이상치에 민감하여 잘못된 분류 가능 |
큰 k (예: k=10, k=20) | 잡음에 덜 민감, 안정적인 결과 제공 | 무관한 데이터의 영향을 받을 가능성이 높고, 결정 경계가 지나치게 부드러워져 정확도가 감소할 수 있음 |
1-3. 일반적인 K값 선택 방법
- 보통 기본적으로 사용하는 k값은 5입니다.
- 실제 문제에서는 데이터셋의 크기와 분포를 고려하여 교차 검증(cross-validation)을 통해 최적의 k값을 찾습니다.
- k는 주로 홀수로 설정하는데, 이는 분류 시 동률(동점)이 발생하는 상황을 피하기 위함입니다.
1-4. KNN 알고리즘의 장점과 단점
장점 | 단점 |
---|---|
- 이해가 쉽고 구현이 간단 | - 데이터가 많아질수록 연산량 증가 |
- 비선형 데이터 및 복잡한 결정 경계에서도 우수한 성능 | - 고차원 데이터에서 성능이 저하되는 "차원의 저주" 문제 발생 가능 |
- 지도 학습에서 강력한 성능 | - K 값(최근접 이웃의 수) 설정에 따라 성능이 크게 좌우됨 |
KNN 알고리즘의 성능을 최대한 활용하기 위해서는 위와 같은 특성을 이해하고, 데이터의 특성에 맞추어 적절한 K값을 선택하는 것이 필수적입니다.
2. KNN 알고리즘의 특징: 게으른 모델(Lazy Model)
KNN은 다른 머신러닝 모델들과 달리 사전에 모델을 학습하지 않고, 단지 훈련 데이터를 저장만 해둡니다. 새로운 데이터가 입력되면 그때서야 주변 이웃 데이터를 찾아 실시간으로 분류합니다. 이렇게 미리 학습하지 않고 필요할 때만 계산한다는 특징 때문에 KNN을 게으른 모델(Lazy Model) 또는 인스턴스 기반 학습(Instance-based learning) 이라고 부릅니다.
알고리즘 | 사전 학습 | 실시간 예측 |
---|---|---|
KNN | 불필요 | 가능 |
SVM, Decision Tree 등 | 필수 | 미리 학습된 모델로 예측 |
장점: 학습 시간이 거의 없고 직관적
단점: 예측 시 매번 계산이 필요해 데이터가 많으면 느려질 수 있음
3. 거리 계산 방법
KNN에서 데이터를 분류할 때 가장 중요한 것은 데이터를 비교하는 데 사용되는 '거리'를 계산하는 방식입니다. 일반적으로 사용하는 거리 계산 방식은 다음과 같이 두 가지입니다.
3-1. 유클리드 거리(Euclidean Distance)
유클리드 거리는 우리가 일반적으로 사용하는 '직선 거리'로, 두 점 사이의 거리를 직선으로 계산하는 방식입니다.
2차원 평면에서 두 점 (x1,y1),(x2,y2) 사이의 거리는 다음과 같이 구합니다:
d=√(x2−x1)2+(y2−y1)2
3차원 이상의 공간에서도 같은 원리로 다음과 같이 계산합니다:
d=√(x2−x1)2+(y2−y1)2+(z2−z1)2+…
위 이미지의 두 점 A(0,0,0)와 B(3,4,5)에 대해 계산하면: d=√(3−0)2+(4−0)2+(5−0)2 =√9+16+25 =√50≈7.07
3-2. 맨해튼 거리(Manhattan Distance)
![]() |
맨해튼 거리 (source) |
맨해튼 거리는 직선 거리가 아닌, 격자 모양의 경로를 따라가는 방식으로 X축과 Y축을 따라 거리를 더한 값입니다. 수식은 다음과 같습니다:
d=|x2−x1|+|y2−y1|
맨해튼 거리는 실제 도시의 도로에서 목적지를 갈 때, 직선으로 가지 않고 도로를 따라 가야 하는 경우와 같은 현실적인 제약 조건에서 자주 사용됩니다.
4. KNN 분류 예시 코드
Iris 데이터셋을 사용하여 KNN 알고리즘을 적용한 분류 예시 코드를 살펴보겠습니다.
4-1. 필요한 라이브러리 임포트
먼저 필요한 라이브러리를 임포트합니다:
import numpy as npimport matplotlib.pyplot as pltimport pandas as pdfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import StandardScalerfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.metrics import accuracy_score, classification_report, confusion_matriximport seaborn as sns
numpy
: 배열 연산을 위해 사용.matplotlib.pyplot
: 시각화를 위한 플롯 생성.pandas
: 데이터셋을 로드하고 데이터프레임으로 처리.sklearn.model_selection.train_test_split
: 데이터셋을 훈련/테스트로 분리.sklearn.preprocessing.StandardScaler
: 데이터 스케일링.sklearn.neighbors.KNeighborsClassifier
: KNN 분류 모델.sklearn.metrics.accuracy_score
,confusion_matrix
,classification_report
: 모델 성능 평가.seaborn
: 혼동 행렬 시각화를 위한 히트맵 생성.
4-2. 데이터 로드 및 준비
Iris 데이터셋을 로드하고, 특징과 레이블을 준비합니다:
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"names = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'Class']dataset = pd.read_csv(url, names=names)X = dataset.iloc[:, :-1].valuesy = dataset.iloc[:, 4].values
pd.read_csv
: UCI 저장소에서 Iris 데이터셋을 다운로드해 데이터프레임으로 로드합니다.names
: 열 이름을 지정합니다(꽃받침 길이/너비, 꽃잎 길이/너비, 클래스).X
: 특징 데이터(4가지 특징: sepal-length, sepal-width, petal-length, petal-width).y
: 타겟 레이블(setosa, versicolor, virginica).
4-3. 훈련/테스트 데이터 분리
모델을 훈련시키고 평가하기 위해 데이터를 훈련 세트와 테스트 세트로 나눕니다:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)
train_test_split
: 데이터를 80%는 훈련용(X_train
,y_train
), 20%는 테스트용(X_test
,y_test
)으로 나눕니다.random_state=42
: 결과 재현성을 위해 랜덤 시드를 고정합니다.
4-4. 데이터 스케일링
KNN은 거리 기반 알고리즘이므로, 특징 값의 스케일이 모델 성능에 큰 영향을 미칩니다. 따라서 StandardScaler
를 사용해 데이터를 표준화합니다:
scaler = StandardScaler()scaler.fit(X_train)X_train = scaler.transform(X_train)X_test = scaler.transform(X_test)
StandardScaler
: 데이터를 평균 0, 표준편차 1로 표준화합니다.fit
: 훈련 데이터에 대해 스케일링 기준(평균, 표준편차)을 학습합니다.transform
: 훈련 데이터와 테스트 데이터를 각각 변환합니다(테스트 데이터는 훈련 데이터의 기준으로 변환).
4-5. KNN 모델 생성 및 훈련
KNN 모델을 생성하고 훈련시킵니다:
classifier = KNeighborsClassifier(n_neighbors=5)classifier.fit(X_train, y_train)
KNeighborsClassifier
: KNN 분류기를 생성합니다.n_neighbors=5
: k=5로 설정해 가장 가까운 5개의 이웃을 기준으로 분류합니다.fit
: 스케일링된 훈련 데이터를 사용해 모델을 학습시킵니다.
4-6. 예측 및 성능 평가
테스트 데이터로 예측을 수행하고 혼동 행렬을 시각화합니다:
y_pred = classifier.predict(X_test)# 혼동 행렬 시각화cm = confusion_matrix(y_test, y_pred)plt.figure(figsize=(8, 6))sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['class_0', 'class_1', 'class_2'], yticklabels=['class_0', 'class_1', 'class_2'])plt.title('Confusion Matrix')plt.xlabel('Predicted')plt.ylabel('Actual')plt.show()
predict
: 테스트 데이터를 사용해 예측값(y_pred
)을 생성합니다.confusion_matrix
: 실제 레이블(y_test
)과 예측 레이블(y_pred
)을 비교해 혼동 행렬을 생성합니다.sns.heatmap
: 혼동 행렬을 히트맵으로 시각화합니다.annot=True
: 각 셀에 숫자를 표시.fmt='d'
: 숫자를 정수로 표시.cmap='Blues'
: 파란색 계열 색상 사용.xticklabels
,yticklabels
: 클래스 이름을 설정(Iris 데이터셋에서는 setosa, versicolor, virginica로 대응).
4-7. 최종 코드
import numpy as npimport matplotlib.pyplot as pltfrom sklearn.datasets import load_winefrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import MinMaxScalerfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.metrics import accuracy_score, confusion_matriximport seaborn as sns# 1. 데이터 로드 (와인 데이터셋)wine = load_wine()X = wine.data[:, [0, 9]] # 간단히 2개의 특징만 사용 (알코올 도수, 색상 강도)y = wine.target# 2. 훈련/테스트 데이터 분리X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)# 3. 데이터 스케일링 (MinMaxScaler 사용)scaler = MinMaxScaler()X_train_scaled = scaler.fit_transform(X_train)X_test_scaled = scaler.transform(X_test)# 4. KNN 모델 생성 및 훈련knn = KNeighborsClassifier(n_neighbors=7, weights='distance') # 거리 가중치 추가knn.fit(X_train_scaled, y_train)# 5. 예측y_pred = knn.predict(X_test_scaled)# 6. 결과 평가accuracy = accuracy_score(y_test, y_pred)print(f"정확도: {accuracy:.2f}")# 7. 혼동 행렬 시각화cm = confusion_matrix(y_test, y_pred)plt.figure(figsize=(8, 6))sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=wine.target_names, yticklabels=wine.target_names)plt.title('Confusion Matrix')plt.xlabel('Predicted')plt.ylabel('Actual')plt.show()# 8. 결정 경계 시각화def plot_decision_boundary(X, y, model): h = 0.02 # step size in the mesh x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1 y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) plt.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm') scatter = plt.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='k') plt.colorbar(scatter) plt.xlabel(wine.feature_names[0]) plt.ylabel(wine.feature_names[9]) plt.title('KNN Decision Boundary (k=7)') plt.show()# 결정 경계 플롯plot_decision_boundary(X_train_scaled, y_train, knn)
5. 결과 분석: 혼동 행렬과 결정 경계 해석
실행 결과로 나온 혼동 행렬과 결정 경계는 다음과 같습니다:
5-1. 혼동 행렬(Confusion Matrix)
5-1-1. 혼동 행렬 분석
- 행(Actual): 실제 클래스 (class_0, class_1, class_2)
- 열(Predicted): 모델이 예측한 클래스
- 대각선 값: 올바르게 예측된 샘플 수 (True Positives)
- 비대각선 값: 잘못 예측된 샘플 수 (False Positives/Negatives)
5-1-2. 세부 분석:
class_0:
- 14개 샘플이 class_0으로 올바르게 예측됨.
- 0개 샘플이 class_1로 잘못 예측됨.
- 1개 샘플이 class_2로 잘못 예측됨.
- 총 15개 샘플 중 14개가 올바르게 분류됨 (정확도: 14/15 = 93.3%).
class_1:
- 0개 샘플이 class_0으로 잘못 예측됨.
- 17개 샘플이 class_1로 올바르게 예측됨.
- 1개 샘플이 class_2로 잘못 예측됨.
- 총 18개 샘플 중 17개가 올바르게 분류됨 (정확도: 17/18 = 94.4%).
class_2:
- 4개 샘플이 class_0으로 잘못 예측됨.
- 0개 샘플이 class_1로 잘못 예측됨.
- 8개 샘플이 class_2로 올바르게 예측됨.
- 총 12개 샘플 중 8개가 올바르게 분류됨 (정확도: 8/12 = 66.7%).
5-1-3. 전체 성능:
- 총 샘플 수: 15 + 18 + 12 = 45개
- 올바르게 예측된 샘플 수: 14 + 17 + 8 = 39개
- 전체 정확도(Accuracy): 39/45 = 0.8667 (약 86.7%)
5-1-4. 추가 지표:
- 정밀도(Precision), 재현율(Recall), F1-Score:
- class_0:
- 정밀도: 14/(14+4) = 0.7778 (class_0으로 예측한 것 중 실제 class_0 비율)
- 재현율: 14/15 = 0.9333 (실제 class_0 중 올바르게 예측한 비율)
- F1-Score: 2 * (0.7778 * 0.9333) / (0.7778 + 0.9333) ≈ 0.8485
- class_1:
- 정밀도: 17/(17+0) = 1.0
- 재현율: 17/18 = 0.9444
- F1-Score: 2 * (1.0 * 0.9444) / (1.0 + 0.9444) ≈ 0.9714
- class_2:
- 정밀도: 8/(8+2) = 0.8
- 재현율: 8/12 = 0.6667
- F1-Score: 2 * (0.8 * 0.6667) / (0.8 + 0.6667) ≈ 0.7273
- class_0:
5-2. 결정 경계(Decision Boundary)
5-2-1. 결정 경계 분석
결정 경계 플롯은 KNN이 두 특징(알코올 도수, 색상 강도)을 기준으로 데이터를 어떻게 분류하는지 보여줍니다:
- 색상 영역: 각 클래스의 경계를 나타냅니다.
- 흰색 영역: class_0
- 분홍색 영역: class_1
- 파란색 영역: class_2
- 데이터 포인트: 훈련 데이터의 실제 클래스(색상)와 위치를 나타냅니다.
- 흰색 원: class_0
- 분홍색 원: class_1
- 파란색 원: class_2
5-2-2. 해석:
- class_0 (흰색 영역): 알코올 도수가 낮고 색상 강도가 낮은 영역에서 주로 분포합니다. 이 클래스는 다른 클래스와 비교적 명확하게 분리되어 있습니다.
- class_1 (분홍색 영역): 알코올 도수가 높고 색상 강도가 높은 영역에서 주로 분포합니다. 이 클래스는 class_2와 일부 겹치는 부분이 있지만, 대체로 잘 구분됩니다.
- class_2 (파란색 영역): 알코올 도수와 색상 강도가 중간에서 높은 영역에서 분포합니다. 이 클래스는 class_1과 경계가 모호한 부분이 많아 혼동이 발생하는 경우가 있습니다.
혼동 행렬에서 class_2의 4개 샘플이 class_0으로 잘못 분류된 이유를 결정 경계에서 확인할 수 있습니다. class_2의 데이터 포인트 중 일부가 class_0의 영역(알코올 도수와 색상 강도가 낮은 곳)에 위치해 있어 모델이 잘못 예측한 것으로 보입니다.
6. 결과 요약 및 개선 방안
6-1. 모델 성능 요약
- 모델은 class_1을 가장 잘 예측했습니다(F1-Score: 0.9714). 정밀도와 재현율 모두 높아 이 클래스에 대해 매우 안정적인 성능을 보입니다.
- class_0도 비교적 잘 예측했습니다(F1-Score: 0.8485). 단, 1개 샘플이 class_2로 잘못 분류되었습니다.
- class_2는 성능이 상대적으로 낮습니다(F1-Score: 0.7273). 특히 4개 샘플이 class_0으로 잘못 분류된 점이 눈에 띕니다. 이는 결정 경계에서 class_2와 class_0의 특징이 일부 겹치는 부분이 있음을 시사합니다.
6-2. 개선 방안
- k값 조정: 현재 k=7로 설정되어 있습니다. k값을 3, 5, 9 등으로 변경하며 성능 변화를 확인해보세요. k값이 너무 크면 과소적합, 너무 작으면 과대적합될 수 있습니다.
- 더 많은 특징 사용: 현재는 알코올 도수와 색상 강도만 사용했지만, 와인 데이터셋의 13가지 특징을 모두 활용하거나 PCA(주성분 분석)를 적용해 차원을 축소해볼 수 있습니다.
- 클래스 불균형 확인: class_2의 샘플 수가 상대적으로 적을 수 있으니, 데이터셋의 클래스 분포를 확인하고 필요하면 오버샘플링(SMOTE 등) 기법을 적용해보세요.
- 스케일링 방법 변경:
MinMaxScaler
대신StandardScaler
를 사용해 성능을 비교해볼 수 있습니다.
7. 추가 분석: k값에 따른 성능 변화
k값에 따라 모델 성능이 어떻게 변하는지 확인하기 위해 아래 코드를 추가해볼 수 있습니다:
k_range = range(1, 16)scores = []for k in k_range: knn = KNeighborsClassifier(n_neighbors=k, weights='distance') knn.fit(X_train_scaled, y_train) y_pred = knn.predict(X_test_scaled) scores.append(accuracy_score(y_test, y_pred))plt.figure(figsize=(8, 6))plt.plot(k_range, scores, marker='o', linestyle='--', color='b')plt.xlabel('k value')plt.ylabel('Accuracy')plt.title('Accuracy vs. k in KNN')plt.grid(True)plt.show()
이 코드는 k값을 1부터 15까지 변경하며 정확도를 계산하고, 결과를 선 그래프로 시각화합니다. 이를 통해 최적의 k값을 찾을 수 있습니다.
7-1. 최적 값 도출
또는, 최적 값을 정확히 출력하는 파이썬 코드를 다음과 같이 삽입할 수도 있습니다.
# scores를 numpy 배열로 변환scores = np.array(scores)# 최대 정확도 찾기max_score = np.max(scores)# 최대 정확도를 가지는 모든 k값 찾기optimal_k_values = [k for k, score in zip(k_range, scores) if score == max_score]# 결과 출력print(f"최적의 k값: {optimal_k_values}, 정확도: {max_score:.2f}")
이 코드를 실행하면 다음과 같은 결과를 얻습니다.
최적의 k값: [1, 2, 6, 7], 정확도: 0.87
위 그래프와 일치합니다.
마치며
KNN은 매우 직관적이고 간단한 알고리즘으로 빠른 예측을 요구하는 여러 분야에서 널리 사용됩니다. 그러나 데이터가 복잡하고 많아질수록 계산량이 증가하고 성능이 저하될 수 있으므로, 상황에 맞는 거리 계산법과 k값 선정이 중요합니다. KNN의 단점은 다른 알고리즘으로 보완하여 보다 강력한 분석 시스템을 구성할 수 있습니다.