Data Park/Python

[Python/ML] K-NN (K-Nearest Neighbors)

Data Park 2023. 1. 10. 18:17

안녕하십니까, '데 박' 입니다.

비전공자들도 쉽게 시작할 수 있는 머신러닝 알고리즘인 K-Nearest Neighbors(K-NN)에 대해 공부해보도록 하겠습니다.

□ K-NN이란

K-NN알고리즘은 분류와 회귀, 모두에서 쓰일 수 있는 간단한 머신러닝 알고리즘입니다.

왜냐하면 K-NN의 모델링 과정은 훈련데이터셋을 그냥 저장하는 것이 과정의 전부입니다.

K-NN은 새로운 데이터 포인트에 대해 예측할 땐 알고리즘이 훈련 데이터셋에서 가장 가까운 데이터 포인트, 즉 "최근접 이웃" 을 찾습니다.

https://machinelearninghd.com/k-nn-k-nearest-neighbors-starter-guide/

SVM은 훈련 데이터를 기반으로 최대 마진을 찾고 결정 경계(Decision Boundary)를 만들어, 이 결정 경계를 통해 테스트 데이터를 분류합니다.

따라서 SVM과 비교하여 K-NN은 사전 모델링이 필요 없고 SVM이나 선형 회귀보다 빠릅니다.

또한 모델을 별도로 구축하지 않는다는 뜻으로 게으른 모델 (Lazy model)이라고 부릅니다.

□ K-NN 분류

이러한 K-NN알고리즘은 가장 가까운 훈련 데이터 포인트에서 K개의 최근접 이웃을 찾아 테스트 데이터 예측에 사용합니다. 자, 아래와 같은 데이터에서 K-NN이 어떻게 동작하는지 이해해보도록 하겠습니다.

import mglearn
import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_moons
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split

X_f , y_f = mglearn.datasets.make_forge()
mglearn.discrete_scatter(X_f[:,0],X_f[:,1],y_f)

# forge 데이터셋에 대해 K=1 최근접 이웃 모델
mglearn.plots.plot_knn_classification(n_neighbors=1)

# forge 데이터셋에 대해 K=3 최근접 이웃 모델
mglearn.plots.plot_knn_classification(n_neighbors=3)

# forge 데이터셋에 대해 K=5 최근접 이웃 모델
mglearn.plots.plot_knn_classification(n_neighbors=5)

둘이상의 이웃을 선택할 때는 레이블을 정하기 위해 투표를 합니다.

즉, 테스트 포인트 하나에 대해 클래스 0이 속한 이웃이 몇 개인지, 그리고 클래스1에 속한 이웃이 몇개인지를 셉니다

그리고 이웃이 더 많은 클래스를 레이블로 지정합니다. 다시말해 K-최근접 이웃중 다수의 클래스가 레이블이 됩니다.

자, K-NN 모델링 과정을 차근차근 알아보겠습니다.

# 데이터 생성
X_f , y_f = mglearn.datasets.make_forge()
# 훈련, 검증 셋 나누기
X_f_train, X_f_test, y_f_train, y_f_test = train_test_split(X_f, y_f, random_state=0) 
# 확인
X_f_train, X_f_test, y_f_train, y_f_test
# 결과
(array([[ 8.92229526, -0.63993225],
        [ 8.7337095 ,  2.49162431],
-- 생략
        [ 9.25694192,  5.13284858],
        [ 8.68937095,  1.48709629]]),
 array([[11.54155807,  5.21116083],
        [10.06393839,  0.99078055],
-- 생략        
        [10.24028948,  2.45544401],
        [ 8.34468785,  1.63824349]]),
 array([0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0]),
 array([1, 0, 1, 0, 1, 1, 0]))
from sklearn.neighbors import KNeighborsClassifier

# 모델 생성
knn_clf = KNeighborsClassifier(n_neighbors=3)

# 모델 학습, KNN이 테스트셋을 분류할 때 이웃을 찾을 수 있도록 데이터를 저장
knn_clf.fit(X_f_train, y_f_train)

# 결과
>>> KNeighborsClassifier(n_neighbors=3)
# 테스트 데이터에 대해 predict 메서드를 호출해서 예측
print("테스트셋 예측:", knn_clf.predict(X_f_test))

>>> 테스트셋 예측: [1 0 1 0 1 0 0]
# score 메서드에 테스트 데이터와 테스트 레이블을 넣어 정확도 출력
print("테스트 셋 정확도 : {:.3f}".format(knn_clf.score(X_f_test, y_f_test)))

>>> 테스트 셋 정확도 : 0.857

이 모델의 정확도는 85.7%입니다.

즉 모델이 테스트 데이터셋에 있는 샘플중 85.7%를 정확히 예측하였다고 할 수 있습니다.

fig,axes = plt.subplots(1,3,figsize=(20,7))
for n_neighbors, ax in zip([1,3,9], axes):
    # KNN 모델 생성
    clf = KNeighborsClassifier(n_neighbors = n_neighbors).fit(X_f, y_f)
    # KNN 결정 경계
    mglearn.plots.plot_2d_separator(clf ,X_f ,fill=True,eps=0.5,ax=ax,alpha=0.4)
    # 산점도
    mglearn.discrete_scatter(X_f[:,0],X_f[:,1],y_f,ax=ax)
    ax.set_title("Neighbors = {}".format(n_neighbors), bbox=dict(facecolor='lime'), fontdict={'size':15})
    ax.set_xlabel("feature 0")
    ax.set_ylabel("feature 1")
axes[0].legend(loc=3)

위의 그림은 이웃이 하나, 셋, 아홉 개일 때의 각 데이터 포인트가 속한 클래스에 따라 평면에 색을 칠하여 알고리즘이 클래스 0과 클래스 1로 지정한 영역으로 나뉘는 결정경계(Decision Boundary)와 산점도를 나타낸 코드입니다.

왼쪽 그림을 보면 이웃을 하나 선택했을때는 결정경계가 훈련 데이터에 가깝게 따라가고있습니다.

이웃의 수를 늘릴수록 결정 경계는 더 부드러워집니다. 부드러운 경계는 더 단순한 모델을 의미합니다.

다시 말해 이웃을 적게 사용하면 모델의 복잡도가 높아지고 많이 사용하면 복잡도는 낮아집니다.

훈련 데이터 전체 개수를 이웃의 수로 지정하는 극단적인 경우에는 모든 테스트 포인트가 같은 이웃(모든 훈련 데이터)을 갖게 되므로 테스트 포인트에 대한 예측은 모두 같은 값이 됩니다.

다른 데이터셋도 적용해보도록 하죠!

# 데이터 생성
from sklearn.datasets import make_circles
from sklearn.datasets import make_moons
X_c, y_c = make_circles(n_samples=100, factor=0.3, noise=0.25, random_state=0)
X_m,y_m = make_moons(n_samples=100, noise=0.25, random_state=0)

from sklearn.datasets import make_classification
X_clf, y_clf = make_classification(
    n_features=2, n_redundant=0, n_informative=2, random_state=1, n_clusters_per_class=1
)
rng = np.random.RandomState(2)
X_clf += 2 * rng.uniform(size=X_clf.shape)
fig, axes = plt.subplots(1,3, figsize=(20,7))
mglearn.discrete_scatter(X_c[:,0],X_c[:,1], y_c , ax=axes[0])
mglearn.discrete_scatter(X_m[:,0],X_m[:,1], y_m , ax=axes[1])
mglearn.discrete_scatter(X_clf[:,0],X_clf[:,1],y_clf, ax=axes[2])

이러한 데이터에서도 K-NN이 잘 작동을 하는지만 볼 것이기때문에 train set과 test set은 분할하지 않겠습니다.

fig,axes = plt.subplots(1,3,figsize=(20,7))
for n_neighbors, ax in zip([1,3,9], axes):
    clf_m = KNeighborsClassifier(n_neighbors = n_neighbors).fit(X_c, y_c)
    mglearn.plots.plot_2d_separator(clf_m ,X_c ,fill=True , eps=0.5, ax=ax, alpha=0.4)
    mglearn.discrete_scatter(X_c[:,0],X_c[:,1],y_c,ax=ax)
    ax.set_title("Neighbors = {}".format(n_neighbors), bbox=dict(facecolor='lightsalmon'), fontdict={'size':15})
    ax.set_xlabel("feature 0")
    ax.set_ylabel("feature 1")
axes[0].legend(loc=3)

fig,axes = plt.subplots(1,3,figsize=(20,7))
for n_neighbors, ax in zip([1,3,9], axes):
    clf_m = KNeighborsClassifier(n_neighbors = n_neighbors).fit(X_m, y_m)
    mglearn.plots.plot_2d_separator(clf_m ,X_m ,fill=True,eps=0.5,ax=ax,alpha=0.4)
    mglearn.discrete_scatter(X_m[:,0],X_m[:,1],y_m,ax=ax)
    ax.set_title("Neighbors = {}".format(n_neighbors), bbox=dict(facecolor='aqua'), fontdict={'size':15})
    ax.set_xlabel("feature 0")
    ax.set_ylabel("feature 1")
axes[0].legend(loc=3)

fig,axes = plt.subplots(1,3,figsize=(20,7))
for n_neighbors, ax in zip([1,3,9], axes):
    clf_clf = KNeighborsClassifier(n_neighbors = n_neighbors).fit(X_clf, y_clf)
    mglearn.plots.plot_2d_separator(clf_clf ,X_clf ,fill=True , eps=0.5, ax=ax, alpha=0.4)
    mglearn.discrete_scatter(X_clf[:,0],X_clf[:,1],y_clf,ax=ax)
    ax.set_title("Neighbors = {}".format(n_neighbors), bbox=dict(facecolor='violet'), fontdict={'size':15})
    ax.set_xlabel("feature 0")
    ax.set_ylabel("feature 1")
axes[0].legend(loc=3)

이 3개의 데이터셋에서도 이웃의 개수에 따라 잘 작동하는 모습을 보실 수 있습니다.

□ K-NN 분류 실습

위에서 이웃을 적게 사용하면 모델의 복잡도가 높아지고 많이 사용하면 복잡도는 낮아진다고 하였는데 Breast Cancer dataset(유방암 데이터)에 적용하여 한번 더 확인해보겠습니다.

from sklearn.datasets import load_breast_cancer

# 데이터 임포트
cancer = load_breast_cancer()

# train, test 분할
X_train, X_test, y_train, y_test = train_test_split(
    cancer.data, cancer.target, stratify=cancer.target, random_state=66
)
# n_neighbors에 따른 정확도를 넣을 array 준비
training_accuracy = []
test_accuracy = []

# 1에서 10까지 n_neighbors를 적용
neighbors_setting = range(1, 11)
for n_neighbors in neighbors_setting:
    # 모델 생성
    clf_cancer = KNeighborsClassifier(n_neighbors=n_neighbors)
    clf_cancer.fit(X_train, y_train)
    # 훈련 셋 정확도 저장
    training_accuracy.append(clf_cancer.score(X_train, y_train))
    # 테스트 셋 정확도 저장
    test_accuracy.append(clf_cancer.score(X_test, y_test))

plt.title("[ KNN Accuracy from n_neighbors ] - Naver_Blog 'Data Park' ")
plt.plot(neighbors_setting, training_accuracy, label="Training Accuracy")
plt.plot(neighbors_setting, test_accuracy, label="Test Accuracy")
plt.ylabel("Accuracy")
plt.xlabel("n_neighbors")
plt.legend()

실제 이런 그래프는 매끈하게 나오지 않지만 여기서도 과대적합과 과소적합의 특징을 볼 수 있습니다.

최근접 이웃의 수가 하나일때 는 훈련 데이터에 대한 예측이 완벽합니다. 하지만 이웃의 수가 늘어나면 모델은 단순해지고 훈련데이터의 정확도는 줄어듭니다.

이웃을 하나 사용한 테스트 셋의 정확도는 이웃을 많이 사용했을 때보다 낮습니다. 이것은 1-최근접 이웃이 모델을 너무 복잡하게 만든다는 것을 설명해줍니다.

반대로 이웃을 10개 사용했을 때는 모델이 너무 단순해서 정확도는 더 나빠지고 정확도가 가장 좋을 때는 중간 정도인 여섯 개를 사용한 경우인 것을 확인하실 수 있습니다.

이상으로 [Python/ML] K-NN (K-Nearest Neighbors) 를 마치도록 하겠습니다..

오늘도 긴 글 읽어주셔서 대단히 감사드립니다