데이터 분석을 향한 발자취 남기기

6. KNN: K Nearest Neighbors 본문

손으로 직접 해보는 모델정리/단일 모델

6. KNN: K Nearest Neighbors

차근차근 해보자 2023. 5. 12. 11:06

오늘은 분류와 회귀에서 쉽게 사용할 수 있는 알고리즘인 KNN에 대해서 알아보고자 한다.

간단한 개념을 바탕으로 하기 때문에 접근하기 쉬운 알고리즘임에도 효과적으로 작동하여 많이 사용되는 알고리즘이다. 

 

1. KNN이란?

2. 주로 사용되는 거리 척도

3. Citrus 분류 예측

 

설명에 사용한 데이터는 오렌지와 자몽을 분류하는 데이터로 이에 대한 설명은 아래 출처를 참고한다.

citrus_500.csv
0.01MB

https://footprints-toward-data-analysis.tistory.com/13

 

[Boosting] AdaBoost Classifier with Citrus

오늘은 이전에 공부했던 AdaBoost Classifier를 구현하여 실제 데이터를 분류해보고 이를 시각화하여 표현해보고자 한다. AdaBoost Classifier에 대한 알고리즘은 아래 링크를 통해 볼 수 있다. https://footpri

footprints-toward-data-analysis.tistory.com

 


1. KNN이란?

KNN (K Nearest Neighbors)는 말 그대로 k개의 가까운 이웃들의 정보를 이용하는 방법이다. 선형회귀모델과 달리, 훈련 데이터의 패턴을 학습하기 보다 테스트 데이터와 유사한 훈련 데이터의 정보를 활용한다. 즉, 모델을 따로 훈련시키지 않기 때문에 선형회귀모델보다 더 빠르고 새로운 데이터를 원활하게 추가할 수 있다는 장점이 있다. 

 

단! 주의할 점은 KNN을 사용할 땐, 피처 간 단위(scale) 차이를 주의깊게 살펴야한다. scale 차이가 크게 발생하는 경우 거리를 계산하는 과정에서 영향을 받아 성능이 저하될 수 있다. 따라서, 일반적으로 피처에 대한 정규화나 표준화를 해준 후, KNN을 사용해준다.

 

KNN에 대한 알고리즘은 다음과 같다.

분류 예제를 통해 이를 이해해보고자 한다.

위 그림은 10개의 훈련 데이터 예제를 나타낸다. 각 데이터는 {+1, -1}로 2개의 클래스를 가짐을 볼 수 있다.

 

이때, 우리가 예측하고 싶은 점을 초록색 별이라고 하자. 우리는 초록색 별이 어떤 클래스를 갖는지를 예측하고자 한다.

 

초록색 별에 대해 고려하고자 하는 이웃 수를 3이라고 설정했을 때, 초록색 별로부터 훈련 데이터간 거리를 모두 계산하고 이중 가장 가까운 3개의 점을 선택한다. 그 결과 +1 클래스는 2개, -1 클래스는 1개가 선택되었다. 초록색 별에 대한 3개 이웃 중 -1 클래스보단 +1 클래스 데이터가 더 많으므로 초록색 별의 클래스는 +1로 예측한다. 

이처럼, KNN은 훈련 데이터를 학습하지 않고 바로 테스트 데이터와의 거리를 계산해 예측함을 볼 수 있다.

 

- 이웃 수 (k)

Step0에서 알 수 있듯이, k는 우리가 직접 선택해줘야하는 값이다. 주의할 점은 k를 너무 작게 설정하면 데이터 하나하나에 영향을 받아 과적합이 발생할 수 있으며, k를 너무 크게 설정하면 분류 경계면이 단조로워져 데이터 구조 파악이 어려워진다. 따라서, KNN으로 예측할 시, 최적의 k값을 찾아 설정해야한다

k에 따른 분류경계선 분

예를 들어, k=1일 때를 보면, -1 클래스의 점을 맞추기 위해 결정 경계가 매우 복잡하게 생성되었음을 볼 수있다. 또한, k=100일 때는 분류 경계선이 대각선처럼 생성되면서 여러 -1 클래스의 데이터를 제대로 맞추지 못하는 것을 볼 수 있다. 이중 k=10일 때, 타당한 분류 경계선이 생성되었음을 볼 수 있다.

 

그럼 이런 최적의 k를 어떻게 설정할 수 있을까?

 

최적의 k를 찾기 위해, 일반적으로 교차 검증을 이용한다. 훈련 데이터를 활용해 일반적으로 가장 좋은 성능을 보이는 k를 찾아 이를 테스트에 그대로 적용하는 것이다. 

 

- 예측값

Step3에서 예측값을 생성하는 방법은 분류와 회귀에 따라 달라진다.

 

- 분류: 이웃들의 class 중 가장 빈도가 큰 class로 예측

- 회귀: 이웃들의 평균값으로 예측

 

일반적으로 KNN을 분류 문제에서 사용할 땐, k를 홀수값으로 설정해주어야한다.

 

KNN 알고리즘 예제와 k에 따른 분류경계선 분포에 대한 코드는 다음과 같다.

'''
KNN 알고리즘 예제
'''

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import NearestNeighbors
import numpy as np

# 데이터 불러오기
data = pd.read_csv("./citrus_500.csv")[:10]

# 훈련 데이터 (X, y)
X = data.loc[:, ["weight", "green"]].values
y = data.name.values

# 새로운 데이터 (1개)
new_value = np.array([179, 76])


'''
훈련 데이터 분포
'''

plt.figure()
plt.scatter(X[:, 0][y == 1], X[:, 1][y == 1], c = "blue", marker = "+", s = 60, label = "class +1")
plt.scatter(X[:, 0][y == -1], X[:, 1][y == -1], c = "red", s = 25, label = "class -1")
plt.legend()
plt.tight_layout() 


'''
새로운 데이터가 추가된 분포
'''

plt.figure()
plt.scatter(X[:, 0][y == 1], X[:, 1][y == 1], c = "blue", marker = "+", s = 60, label = "class +1")
plt.scatter(X[:, 0][y == -1], X[:, 1][y == -1], c = "red", s = 25, label = "class -1")
plt.scatter(new_value[0], new_value[1], c = "green", marker = "*", s = 60, label = "new point")
plt.legend()
plt.tight_layout() 


'''
가장 가까운 3개 이웃을 
표시한 분포
'''

# 3개의 이웃 선택 
nn = NearestNeighbors(n_neighbors = 3)
nn.fit(X, y)

# 가장 가까운 3개 이웃에 대한 거리와 위치
distance, index = nn.kneighbors(new_value.reshape(1, -1))

plt.figure()
for i in index[0]:
    neighbor = X[i]
    plt.plot([new_value[0], neighbor[0]], [new_value[1], neighbor[1]], color = "black", zorder = 1)
plt.scatter(X[:, 0][y == 1], X[:, 1][y == 1], c = "blue", marker = "+", s = 60, label = "class +1")
plt.scatter(X[:, 0][y == -1], X[:, 1][y == -1], c = "red", s = 25, label = "class -1")
plt.scatter(new_value[0], new_value[1], c = "green", marker = "*", zorder = 2, s = 60, label = "new point")
plt.legend()   
plt.title("neighbors(k) = 3", fontsize = 12, fontweight = "bold")
plt.tight_layout()
'''
k에 따른 
분류경계선 분포
'''

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier
from sklearn.inspection import DecisionBoundaryDisplay
from matplotlib.colors import ListedColormap

# 데이터 불러오기
data = pd.read_csv("./citrus_500.csv")

# 훈련 데이터 (X, y)
X = data.loc[:, ["weight", "green"]].values
y = data.name.values

# +1 -> 파랑, -1 -> 빨강
color = ["red" if i == 0 else "blue" for i in y]
marker = ["o" if i == 0 else "+" for i in y]

# 분류 경계선 색 지정
cmap_light = ListedColormap(["lightcoral", "lightsteelblue"])

# 이웃 수(k) 지정
k_list = [1, 10, 100]

fig, ax = plt.subplots(nrows = 1, ncols = 3, figsize = (15, 5))

for idx, i in enumerate(k_list):
    
    # knn 모델을 이용한 예측 생성
    knn = KNeighborsClassifier(n_neighbors = i)
    knn.fit(X, y)
    pred_x = knn.predict(X)
    
    # 분류 경계선 그리기
    DecisionBoundaryDisplay.from_estimator(
        knn,
        X,
        ax = ax[idx],
        cmap=cmap_light,
        response_method="predict",
        plot_method="pcolormesh",
        shading="auto",
    )
    
    # 데이터 분포 그리기
    ax[idx].scatter(X[:, 0][y == 1], X[:, 1][y == 1], c = "blue", marker = "+", s = 60, label = "class +1")
    ax[idx].scatter(X[:, 0][y == -1], X[:, 1][y == -1], c = "red", s = 25, label = "class -1")
    ax[idx].set_title("k = %d"%i, fontweight = "bold", fontsize = 15)
    ax[idx].set_xlabel("weight", fontsize = 13)
    ax[idx].set_ylabel("green", fontsize = 13)
    ax[idx].legend()
    plt.tight_layout()

2. 주로 사용되는 거리 척도

각 테스트 데이터에 대해, 모든 훈련 데이터와의 거리를 계산해야한다. 거리를 계산하는 방법으로 다양하게 있지만, 이중 6개의 거리 계산 척도를 보고자 한다.

 

- 유클리디안 거리 (Euclidean distance)

▷ 평면 또는 다차원 공간에 표시할 수 있는 피처에 대한 거리를 표시할 수 있으며, KNN 알고리즘에서 가장 많이 쓰인다.

▷ 단점: 단위에 따라 결과값이 매우 민감하게 달라진다. 

 

- 맨해튼 거리 (Manhattan distance)

▷ 두 개의 k차원 실수 벡터 간 거리를 구할 때 사용한다.

▷ 격자모향의 도심거리를 다니는 택시 (택시는 무조건 차량도로로만 다닐 수 있음)와 유사함

 

- p차 민코프스키 거리 (Minkowski distance)

p는 하이퍼 파라미터이다. (사용자가 직접 설정해줘야하는 값)

p=1이면 Manhattan distance, p=2이면 Euclidean distance를 나타냄

 

- 마할라노비스 거리 (Mahalanobis distance)

Euclidean distance에서 피처들의 공분산을 반영하여 거리를 계산하는 방법으로 단위와 무관하다.


3. Citrus 분류 예측

knn 알고리즘을 이용해 citrus 데이터를 분류하고자 한다. 이때, train, test 비율은 7:3으로 두고 직접 짠 hand knn과 sklearn의 모듈을 사용한 sklearn knn간 예측값 결과를 비교하였다. 그 결과, 두 모델로부터 얻은 예측값은 일치하였으며, 이를 통해 직접 짠 hand knn이 잘 생성되었음을 알 수 있다. 

 

거리는 유클리디안 거리를 사용하였으며, 분류이기 때문에 최종 예측은 클래스의 최빈값으로 사용하였다. 이를 회귀에 적용하기 위해서는 최종 예측 시, 평균으로 바꿔주기만 하면 된다!

import pandas as pd
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
import numpy as np
from scipy.stats import mode
from sklearn.metrics import accuracy_score

# 데이터 불러오기
data = pd.read_csv("./citrus_500.csv")

# 훈련 데이터 (X, y)
X = data.loc[:, ["weight", "green"]].values
y = data.name.values


Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, test_size = 0.3, random_state = 0) 


'''
Hand KNN
'''    

# 예측하여 나온 값 담을 빈 리스트 만들기
predict = []

# euclidean distance를 이용해 각 test와 전체 훈련 데이터 간 거리 계산 (test 개수 x train 개수)
distance = [np.sqrt(np.sum((Xtrain-x_test_point)**2, axis = 1)) for x_test_point in Xtest]

# 가장 작은 거리를 갖는 index부터 순서대로 나열한 후 5개 선택
index = np.array([np.argsort(dist)[:5] for dist in distance])

# 이웃들의 클래스 중 최빈값을 최종 예측값으로 사용함
hand_pred_value = np.array([mode(ytrain[idx], keepdims = False)[0] for idx in index])

# 성능
print("Accuracy score:", round(accuracy_score(ytest, hand_pred_value), 4))


'''
Sklearn KNN
'''

knn = KNeighborsClassifier(n_neighbors=5, metric = "euclidean")
knn.fit(Xtrain, ytrain)
knn_pred_value = knn.predict(Xtest)

# hand와 sklearn의 knn 예측값이 서로 다른 경우 출력
for idx, i, j in zip(range(len(hand_pred_value)), hand_pred_value, knn_pred_value):
    if i != j:
        print(idx)

# 성능
print("Accuracy score:", round(accuracy_score(ytest, knn_pred_value), 4))

오늘은 접근하기도 쉽고 이해하기도 쉬운 KNN 알고리즘에 대해서 알아보았다. 

다음 시간에는 트리 관련 모델에 대해서 설명하고자 한다.

Comments