지난 2023.04.18 - [Programming/특성 공학] - [Machine Learning] 지도 학습의 종류
에 이어 먼저 붓꽃 분류를 위해 사용했었던 k-최근접 이웃(k-NN) 알고리즘부터 다시 시작 해 보겠습니다.
K-NN은 무엇인가
k-NN(k-Nearest Neighbors) 알고리즘은 제일 간단한 머신러닝 알고리즘 입니다. 단순히 훈련할 데이터셋(training data set) 저장하는 것이 머신러닝 모델을 만드는 것이 전부입니다.
새로운 데이터 포인트를 예측할 때는 훈련 데이터셋으로 만들어 놓은 k-NN 모델에서 가장 가까운 데이터 포인트, 즉 최근접 이웃을 찾습니다.
#필요 라이브러리 임포트
from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
plt.rcParams['axes.unicode_minus'] = False
import platform
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
font_name = font_manager.FontProperties(fname=path).get_name()
rc('font', family=font_name)
else:
print('Unknown system... sorry!')
K-NN 분류 모델
mglearn 라이브러리에 이미 forge 데이터 셋을 이용해 k-NN 알고리즘을 적용하고 나서 시각화 한 내용이 있습니다. 바로 확인해 보죠!
mglearn.plots.plot_knn_classification(n_neighbors=1) # n_neighbors : 이웃의 갯수
뭔가 복잡한 것 같지만, 모양으로 한번 확인해 보겠습니다.
동그라미 모양의 데이터들은 훈련 데이터입니다. 입니다. k-NN 모델 객체에 훈련시킨 데이터가 표시되어 있습니다.
별 모양의 데이터들은 테스트 데이터로써, 선을 이용해 각 훈련된 데이터 포인트에서 가장 가까운 이웃과 선이 연결되어 있습니다.
n_neighbors 파라미터의 값을 변경하면 가장 가까운 여러 개의 이웃이 선택되고, 둘 이상의 이웃이 선택되면 레이블( 분류 결과 - 그래프에서는 색상 )을 정하기 위해 투표를 합니다.
즉 테스트 포인트 하나에 대해서 클래스 0( 파란색 )에 속한 이웃이 몇 개인지, 클래스 1( 주황색 )에 속한 이웃이 몇 개인지를 셉니다. 그리고 이웃이 더 많은 클래스를 레이블로 지정합니다.
정리하자면 테스트 데이터에 대해 k-최근접 이웃 중 다수의 클래스가 레이블이 된다고 볼 수 있습니다. 이어서 3개의 최근접 이웃을 사용한 예를 보겠습니다.
mglearn.plots.plot_knn_classification(n_neighbors=3) # 이웃이 3개
이웃의 개수 ( n_neighbors 파라미터 )를 3개로 조절했더니 그래프 상에서는 별 모양의 테스트 데이터에 대해 3개의 훈련 데이터 포인트가 연결된 것을 확인할 수 있습니다.
또한 이웃을 한 개만 이용했을 때와, 세 개의 이웃을 사용했을 때 결과가 달라지는 것도 확인할 수 있습니다.
단순한 특성 1, 특성 2를 활용한 단순한 이진 분류 문제이지만, 우리가 k-NN 모델을 사용한다면 클래스가 다수인 데이터셋에도 같은 방법을 적용할 수 있습니다.
클래스가 여러 개일 때도 각 클래스에 속한 이웃이 몇 개인지를 헤아려 가장 많은 클래스를 예측값으로 사용합니다. 본격적으로 k-NN 알고리즘 적용에 대해 알아보겠습니다.
from sklearn.model_selection import train_test_split # 일반화 성능 평가를 위해 기존 데이터를 훈련 세트와 테스트 세트로 나눔
X, y = mglearn.datasets.make_forge()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
모델을 만드는 방법은 간단합니다. k-NN 이웃 알고리즘은 scikit-learn에서 KNeighborsClassifier로 제공되고 있습니다
from sklearn.neighbors import KNeighborsClassifier #k-NN 분류 임포트
clf = KNeighborsClassifier(n_neighbors = 3) # 이웃의 갯수를 3개 갖는 모델 생성
이어서 훈련 세트를 사용해 KNeighborsClassifier 모델 객체( clf )를 훈련( fit ) 시켜 봅시다.
clf.fit(X_train, y_train) # 훈련하기
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=3, p=2, weights='uniform')
잘 훈련되었으면 테스트 데이터에 대해 예측( predict ) 해보겠습니다.
print("테스트 세트 예측: {}".format(clf.predict(X_test)))
테스트 세트 예측: [1 0 1 0 1 0 0]
방금 만든 모델이 얼마나 잘 일반화되었는가를 판단하기 위해 score 메소드에 테스트 데이터와 테스트 레이블을 넣어서 확인합니다.
print("테스트 세트 정확도: {:.2f}".format(clf.score(X_test, y_test)))
테스트 세트 정확도: 0.86
우리가 만든 모델의 정확도는 86%가 나왔습니다. 즉 모델이 테스트 데이터셋에 있는 샘플 중 86%를 정확히 예측한 것입니다.
이어서 이웃의 수에 따라서 k-NN 모델이 어떻게 데이터를 구분하는지 평면에 색을 칠해 경계를 나눠 보겠습니다.
이처럼 클래스 0과 클래스 1로 지정한 영역으로 나뉘는 경계를 결정 경계 (decision boundary)라고 합니다
fig, axes = plt.subplots(1, 5, figsize=(20,3))
for n_neighbors, ax in zip([1,3,6,9,26], axes):
# fit 메소드는 self 반환을 하기 때문에 객체 생성과 메소드를 한줄에 사용 할 수 있습니다.
clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X, y)
mglearn.plots.plot_2d_separator(clf, X, fill=True, eps=0.5, ax=ax, alpha=.4)
mglearn.discrete_scatter(X[:,0], X[:,1],y, ax=ax)
ax.set_title("{} 이웃".format(n_neighbors))
ax.set_xlabel("특성 0")
ax.set_ylabel("특성 1")
axes[0].legend(loc=3)
<matplotlib.legend.Legend at 0x1 c14 acab38>
위의 그래프를 한번 분석해 보겠습니다.
- 이웃이 1개인 경우는 결정 경계가 훈련데이터에 가깝게 나눠지고 있습니다. 즉 모델의 복잡도가 증가했습니다. (과대적합 가능성)
- 이웃이 3개인 경우는 결정 경계가 1개일 때보다 약간 부드러워졌네요. 이는 모델의 복잡도가 감소한 것을 의미합니다.
- 이웃이 6개인 경우는 결정 경계가 클래스들이 위치한 것과 비슷한 구역을 각각 나눠 갖은것 같습니다.
- 이웃이 9개인 경우는 결정 경계가 6개 일 때 보다 훨씬 완만해집니다. 가운데쯤에 있는 특성 1 때문에 그런 것 같습니다. 모델이 점점 더 단순해지는 것 같네요( 과소 적합 가능성)
- 극단적으로 26개의 이웃을 가진 경우는 모든 데이터를 하나로 분석해 버립니다. k-nn은 이웃의 개수로 투표를 하기 때문인 것 같습니다.
위의 분석 내용과 그래프로 알 수 있는 사실은 모델의 개수가 너무 적으면 복잡도가 증가해 과대적합 가능성이 생겨버리고, 모델의 개수가 많으면 복잡도가 감소해 과소 적합 가능성이 생긴 다는 것을 알 수 있습니다.
유방암 데이터를 이용해 k-NN 알아보기
모델의 복잡도와 일반화 사이의 관계를 입증해 보도록 하겠습니다. 이웃의 개수에 따라 얼마나 k-NN이 잘 일반화되는지를 알아보겠습니다.
먼저 유방암 데이터에 대해 훈련 세트와 테스트 세트를 준비해 보겠습니다. 그러고 나서 이웃의 개수를 따로 하여 얼마나 정확도가 증가하고 알맞은 모델이 되는지 평가해 보겠습니다.
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer() #유방암 데이터 불러오기
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target,
stratify=cancer.target, random_state=66) # 테스트 세트와 훈련 세트 분할
training_accuracy = [] # 각 이웃 개수 별 훈련 세트에 대한 정확도를 저장할 리스트
test_accuracy = [] # 각 이웃 개수 별 테스트 세트에 대한 정확도를 저장할 리스트
#이웃의 개수 설정 (1개 ~ 10개 까지)
neighbors_settings = range(1, 11)
for n_neighbors in neighbors_settings:
#모델 생성하기
clf = KNeighborsClassifier(n_neighbors=n_neighbors)
clf.fit(X_train, y_train)
#훈련 세트 정확도 저장
training_accuracy.append(clf.score(X_train, y_train))
#일반화 정확도 저장
test_accuracy.append(clf.score(X_test, y_test))
plt.plot(neighbors_settings, training_accuracy, label="훈련 정확도") # 이웃 개수에 대한 훈련 세트 정확도 선 그래프 그리기
plt.plot(neighbors_settings, test_accuracy, label="테스트 정확도") # 이웃 개수에 대한 테스트 세트 정확도 선 그래프 그리기
plt.ylabel("정확도")
plt.xlabel("n_neighbors")
plt.legend()
<matplotlib.legend.Legend at 0x102 cdf5 f8>
그래프를 보면 과대적합과 과소적합의 특성을 알 수 있습니다.
- 이웃의 개수가 적으면 훈련세트의 정확도는 높지만 테스트 세트의 정확도는 낮고 - 과대적합
- 이웃의 개수가 많아지면 모델이 단순해지면서 훈련 정확도와 테스트 정확도가 동시에 낮아집니다. - 과소적합
여기서 알 수 있는 사실은 이웃이 1개인 경우에는 너무 모델을 복잡하게 만들어 낸다는 사실을 알 수 있네요, 반대로 이웃을 10개 사용했을 때는 모델이 너무 단순해서 정확도가 더욱더 나빠진다는 사실을 알 수 있게 됩니다.
가장 좋은 이웃은 몇 개일까요?테스트 세트의 정확도가 가장 높은 6개 정도의 이웃을 사용했을 때 가장 일반화가 잘 됐다라고 할 수 있겠습니다.
K-NN Regression
k-NN 알고리즘은 회귀 분석을 위해서도 사용합니다. wave 데이터셋을 이용해 알아보도록 하죠.
분류와 마찬가지로 mglearn 패키지에 wave 데이터 셋을 이용한 회귀 시각화 그래프가 이미 존재합니다. 먼저 이웃이 한 개인 경우의 회귀 결과입니다.
mglearn.plots.plot_knn_regression(n_neighbors=1)
먼저 파란색 원은 트레이닝 데이터, 초록색 별은 입력한 특성, 마지막으로 파란색 별은 입력한 특성에 대한 회귀 분석 결과입니다.
k-NN 회귀는 이웃의 개수를 다수로 지정하면 이웃 간의 평균이 그 예측값이 됩니다.
mglearn.plots.plot_knn_regression(n_neighbors=3)
이웃의 개수가 한 개일 때는 단순하게 제일 가까운 이웃만을 따져서 파란색 별의 위치가 결정되었지만, 이웃의 개수가 3개가 되면 가장 가까운 3개의 점을 찾아 그 평균을 회귀 결괏값으로 사용하는 것을 알 수 있습니다.
k-NN 회귀는 k-NN 분류와 비슷하게 사용할 수 있습니다. KNeighborsRegressor에 구현되어 있습니다.
from sklearn.neighbors import KNeighborsRegressor # k-NN 회귀를 위한 KNeighborsRegressor 임포트
X, y = mglearn.datasets.make_wave(n_samples=40)
#wave 데이터셋을 훈련 세트와 테스트 세트로 나누기
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 이웃의 수를 3으로 하여 모델 객체 생성하기
reg = KNeighborsRegressor(n_neighbors = 3)
# 훈련 데이터와 타깃을 사용하여 모델 학습 시키기
reg.fit(X_train, y_train)
print("테스트 세트 예측 :\n{}".format(reg.predict(X_test)))
테스트 세트 예측 :
[-0.05396539 0.35686046 1.13671923 -1.89415682 -1.13881398 -1.63113382
0.35686046 0.91241374 -0.44680446 -1.13881398]
이 역시 score 메소드를 이용해 점수를 알 수 있는데요, score 메소드는 회귀분석일 때는 $R^2$ 값을 반환합니다.
여기서 $R^2$ 값은 결정 계수 라고 하며 회귀 모델에서의 예측의 적합도를 0 ~ 1 사이의 값으로 계산한 것입니다. 공식은 다음과 같습니다. \begin {align} \\R^2=1-\frac {\sum{({y-\hat {y})^2}}}{\sum{({y-\bar {y})^2}}} \end {align}
의미하는 바는 다음과 같습니다. $y$ 는 타깃값이고, $\bar {y}$는 타깃값($y$ 들)의 평균이며, $\hat {y}$는 모델의 예측값입니다.
즉 $y$는 우리가 맞춰야 할 값( 훈련된 데이터 ), $\bar {y}$는 훈련된 데이터들의 평균값, $\hat {y}$는 우리가 만든 모델이 테스트 데이터에 대해 예측한 값입니다.
따라서 훈련데이터들(X_train)의 타깃값으로 사용한 y_train을 예측 데이터로 사용하게 되면 $R^2$의 공식에서 $\hat {y}$이 $\bar {y}$와 같게 되고, 분자와 분모가 같아져 $R^2$값이 0이 되게 됩니다.
1은 예측이 완벽한 경우, 0은 훈련 세트의 출력값인 y_train의 평균으로만 예측하는 모델의 경우입니다.
print("테스트 세트 R^2 : {:.2f}".format(reg.score(X_test, y_test)))
테스트 세트 R^2 : 0.83
우리가 만든 모델에서 예측한 값은 83% 정도로 비교적 잘 맞는 것 같네요!
wave 데이터셋을 계속 이용하여 이웃의 개수(n_neighbors)에 따른 테스트 세트의 성능을 판단해 보겠습니다.
fig, axes = plt.subplots(1,4, figsize=(20,4))
#-3과 3 사이에 1000개의 데이터 만들기 -> 테스트 용도로 사용함
line = np.linspace(-3, 3, 1000).reshape(-1, 1)
for n_neighbors, ax in zip([1,3,5,9], axes):
# 이웃의 개수를 1,3,9로 하여 예측하는 모델을 만듭니다
reg = KNeighborsRegressor(n_neighbors=n_neighbors)
reg.fit(X_train, y_train)
ax.plot(line, reg.predict(line)) # 테스트 용도로 만든 데이터를 예측하고 예측 결과를 선으로 표현합니다.
ax.plot(X_train, y_train, '^', c=mglearn.cm2(0), markersize=8) # 훈련 데이터를 그래프에 표시합니다.
ax.plot(X_test, y_test, 'v', c=mglearn.cm2(1), markersize=8) # 테스트 데이터를 그래프에 표시합니다.
# 훈련 데이터의 점수와 테스트 데이터의 점수를 제목에 표현합니다.
ax.set_title("{} 이웃의 훈련 스코어: {:.2f} / 테스트 스코어 : {:.2f}".format(n_neighbors, reg.score(X_train, y_train), reg.score(X_test, y_test)))
ax.set_xlabel("특성")
ax.set_ylabel("타깃")
axes[0].legend(["모델 예측", "훈련 데이터/타깃", "테스트 데이터/타깃"], loc="best")
<matplotlib.legend.Legend at 0x1 c1744 d588>
첫 번째 그래프부터 확인해 보겠습니다.
이웃의 개수가 1개인 첫 번째 그래프는 모델을 예측한 라인들이 모두 훈련 데이터를 지나가는 것을 확인할 수 있습니다. 이는 훈련 세트의 각 데이터 포인트들이 예측에 주는 영향이 커서 매우 불안정한 예측이라고 할 수 있습니다.
이에 반에 이웃의 개수가 많아질수록 훈련 스코어가 점점 줄어드는 것을 볼 수 있는데 이는 이웃을 많이 사용할수록 훈련 데이터에는 잘 맞지 않을 수 있지만, 더 안정적으로 예측을 할 수 있게 됩니다.
이웃의 개수가 3개 일 때는 적절히 훈련 데이터와 테스트 데이터를 지나가는 모습을 볼 수 있으며, 그 이상의 이웃이 설정될수록 예측 라인들이 훈련데이터를 빗겨 지나가는 모습을 확인할 수 있습니다. (정확도가 점점 떨어집니다.)
장단점과 매개변수
우리는 지금까지 한 개의 매개변수(n_neighbors)만 사용해서 이웃의 개수를 조절하면서 모델의 성능을 체크해 보았습니다. 보통이면 두 개의 매개변수를 활용하는데, 다른 하나는 거리 조절 공식을 따로 준비하는 것입니다.
기본적으로 k-NN은 metic 매개변수를 활용하여 거리 측정 방식을 변경할 수 있으며, 기본값은 민코프스키 거리를 의미하는 'minkowski'가 설정되어 있습니다.
이 민코프스키 거리를 제곱하여 크기를 정하는 p의 기본값이 2일 때 k-NN의 기본 거리 측정 공식인 유클라디안 거리와 같게 됩니다.
k-NN의 장점은 이해하기가 매우 쉬운 모델이라는 점입니다. 또한 많이 조정할 필요 없이 좋은 성능을 발휘하여 k-NN 보다 더 복잡한 알고리즘을 적용하기 전에 테스트해볼 수 있는 좋은 시작점이 됩니다.
단점은 모델 자체는 매우 빠르게 만들어 볼 수 있지만, 훈련 세트가 매우 크면( 특성 또는 샘플 데이터의 개수가 많으면 ) 예측이 느려지게 됩니다. 또한 수백 개의 특성을 가진 데이터 셋에는 잘 동작하지 않고, 희소한 데이터셋 ( 특성값의 대부분이 0인 ) 데이터셋과는 특히 잘 작동할 수 없습니다.
위의 단점에 따라 k-NN은 예측이 느리고 많은 특성을 처리하는 능력이 부족하기에 현업에서는 잘 사용하지 않습니다.
선형모델을 통해 k-NN 알고리즘의 단점을 극복할 수 있습니다.
다음 블로그에서는 선형 회귀에 대하여 알아보도록 하겠습니다.
'Programming > 특성 공학' 카테고리의 다른 글
[Machine Learning] 선형 이진 분류 (0) | 2023.05.09 |
---|---|
[Machine Learning] 선형 회귀 (0) | 2023.05.02 |
[Machine Learning]지도 학습의 종류 (0) | 2023.04.18 |
[Machine Learning]지도 학습 (0) | 2023.04.14 |
[데이터 전처리]수치 변환 (1) | 2023.04.04 |