본문 바로가기

머신러닝

[머신러닝] K-최근접 이웃 회귀 (K-NN Regression) 개념 및 실습

반응형

 

[목차]

 

1. K-NN Regression

 

2. KNeighborsRegressor Class

 

3. K-NN Regression 실습

 

4. 과대적합 vs 과소적합

 

5. K-NN Regression의 한계

 

 

  1. K-NN Regression

 

이전에는 생선이 도미에 속하는지 빙어에 속하는지 '분류'하는 문제를 다루었다. (https://rebro.kr/183)

 

다른 문제로 생선의 크기나 길이를 보고 생선의 무게를 예측하는 문제도 다룰 수 있다. 이처럼 클래스 중 하나로 분류하는 것이 아니라 임의의 어떤 '숫자'를 예측하는 문제회귀(Regression)라고 한다. 

 

그중 K-NN Regression(K-최근접 이웃 회귀) 알고리즘주변의 가장 가까운 K개의 샘플을 통해 값을 예측하는 방식이다. 예를 들어 가장 간단한 방식으로는 K개 샘플의 평균을 이용할 수 있다. 

 

 

  2. KNeighborsRegressor Class

 

실습에 앞서, 먼저 K-NN 회귀 알고리즘이 구현된 클래스인 KNeighborsRegressor에 대해 알아보자. 

 

https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html

 

sklearn.neighbors.KNeighborsRegressor

Examples using sklearn.neighbors.KNeighborsRegressor: Face completion with a multi-output estimators Face completion with a multi-output estimators, Imputing missing values with variants of Iterati...

scikit-learn.org

 

KNeighborsRegressor 클래스는 다음과 같이 구성되어 있다. 

class sklearn.neighbors.KNeighborsRegressor(n_neighbors=5, *, weights='uniform', algorithm='auto', 
leaf_size=30, p=2, metric='minkowski', metric_params=None, n_jobs=None)

 

 

n_neighbors : int

 

이웃의 수인 K를 결정한다. default는 5다. 

 

weights : {'uniform', 'distance'} or callable

 

예측에 사용되는 가중 방법을 결정한다. default는 uniform이다. 

'uniform' : 각각의 이웃이 모두 동일한 가중치를 갖는다. 

'distance' : 거리가 가까울수록 더 높은 가중치를 가져 더 큰 영향을 미치게 된다.

callable : 사용자가 직접 정의한 함수를 사용할 수도 있다. 거리가 저장된 배열을 입력으로 받고 가중치가 저장된 배열을 반환하는 함수가 되어야 한다. 

 

algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'} 

 

가장 가까운 이웃들을 계산하는 데 사용하는 알고리즘을 결정한다. default는 auto이다. 

'auto' : 입력된 훈련 데이터에 기반하여 가장 적절한 알고리즘을 사용한다. 

'ball_tree' : Ball-Tree 구조를 사용한다. (Ball-Tree 설명 : https://nobilitycat.tistory.com/entry/ball-tree)

'kd_tree' : KD-Tree 구조를 사용한다.

'brute' : Brute-Force 탐색을 사용한다. 

 

▶ leaf_size : int

 

Ball-Tree나 KD-Tree의 leaf size를 결정한다. default값은 30이다.

이는 트리를 저장하기 위한 메모리뿐만 아니라, 트리의 구성과 쿼리 처리의 속도에도 영향을 미친다. 

 

▶ p : int

 

민코프스키 미터법(Minkowski)의 차수를 결정한다. 예를 들어 p = 1이면 맨해튼 거리(Manhatten distance), p = 2이면 유클리드 거리(Euclidean distance)이다. 

 

 

  3. K-NN Regression 실습 

 

농어의 무게를 예측하는 프로그램을 작성해보자. 

실습 내용은 '혼자 공부하는 머신러닝+딥러닝' 책을 이용하였다.

 

먼저 훈련 데이터를 준비하자. 데이터는 data.py 모듈에 따로 저장하였다. 

perch_length = np.array([8.4, 13.7, 15.0, 16.2, 17.4, 18.0, 18.7, 19.0, 19.6, 20.0, 21.0,
       21.0, 21.0, 21.3, 22.0, 22.0, 22.0, 22.0, 22.0, 22.5, 22.5, 22.7,
       23.0, 23.5, 24.0, 24.0, 24.6, 25.0, 25.6, 26.5, 27.3, 27.5, 27.5,
       27.5, 28.0, 28.7, 30.0, 32.8, 34.5, 35.0, 36.5, 36.0, 37.0, 37.0,
       39.0, 39.0, 39.0, 40.0, 40.0, 40.0, 40.0, 42.0, 43.0, 43.0, 43.5,
       44.0])
perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

농어의 길이만으로 무게를 예측할 것이다. 따라서 농어의 길이가 특성이고 무게가 타깃이 된다. 

산점도를 그려보면 당연하게 농어의 길이가 커짐에 따라 무게가 늘어나는 것을 볼 수 있다.

이제 농어 데이터를 훈련 셋과 테스트 셋으로 나누어보자. 이를 위해 사이킷런(scikit-learn)의 train_test_split() 함수를 사용한다. 

from sklearn.model_selection import train_test_split

train_input, test_input, train_target, test_target = train_test_split(perch_length, perch_weight, random_state = 42)

 

이전 게시글에서는 직접 랜덤과 인덱스를 이용하여 셋을 분리했지만, 위 함수를 사용하면 간편하게 분리해낼 수 있다. 

train_test_split 함수는 다음과 같이 구성되어 있다. 

sklearn.model_selection.train_test_split(*arrays, test_size=None, train_size=None, 
random_state=None, shuffle=True, stratify=None)

 

array에는 동일한 길이의 리스트, 넘파이 배열, 판다스 dataframe 등의 집합이 들어간다. 

test_size는 0.0 ~ 1.0 사이의 실수 값이 들어가고, 이는 테스트 셋의 크기 비율을 설정한다. default는 0.25로, 전체 중 25프로의 샘플을 테스트 셋으로 뽑는다. 

train_size도 test_size와 동일하게 훈련 셋의 비율을 설정하고, default는 테스트 셋을 제외한 나머지다. 

 

사이킷런에 사용할 훈련 셋은 2차원 배열이어야 한다. 하지만 perch_length가 1차원 배열이기 때문에 train_input과 test_input도 1차원 배열이다. 따라서 이를 1개의 열로 구성된 2차원 배열로 바꿔주어야 한다. 

넘파이 배열의 크기를 바꾸는 메서드는 reshape()가 있다.  

a = np.array([1, 2, 3, 4])
print(a) # [1 2 3 4]

a = a.reshape(2, 2)
print(a) 
# [[1 2] 
# [3 4]]

a = a.reshape(2, 3) # ERROR

 

다만 reshape 메서드는 지정한 크기가 원본 배열에 있는 원소의 개수와 다르면 에러가 발생한다. 

배열의 크기를 자동으로 지정하는 기능도 제공한다. 크기를 -1로 지정하면 나머지 원소 개수로 모두 채워진다. 이를 사용하면 배열의 원소의 개수를 정확히 모르더라도 크기를 쉽게 변경할 수 있다.  

a = np.array([1, 2, 3, 4])
print(a) # [1 2 3 4]

a = a.reshape(-1, 2)
print(a) 
# [[1 2] 
# [3 4]]

a = a.reshape(-1, 1)
print(a)
#[[1]
# [2]
# [3]
# [4]]

 

이를 이용해서 훈련 셋과 테스트 셋을 2차원으로 바꿔주자. 

train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

 

이제 앞에서 설명한 KNeighborsRegressor 클래스를 통해 회귀 모델을 훈련할 수 있다. 

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor()
knr.fit(train_input, train_target)

print(knr.score(test_input, test_target)) # 0.992809406101064

 

훈련하는 방법은 분류와 비슷하다. 테스트 셋의 점수를 확인해보니 0.9928... 로 나온다. 

분류에서는 이 점수가 맞은 샘플의 개수(비율)를 의미했는데, 회귀에서는 어떤 값을 의미할까? 

 

회귀에서는 이 점수를 결정계수(Coefficient of determination)라고 부른다. 또는 간단하게 $R^2$라고도 부른다. 계산 식은 다음과 같다. 

$R^2 = 1 - \frac{(타깃 - 예측)^2}{(타깃 - 평균)^2}$

 

만약 타깃의 평균 정도를 예측하는 수준이라면 $R^2$는 0에 가까워지고, 타깃이 예측에 아주 가까워지면 1에 가까운 값이 된다. 따라서 0.99면 매우 좋은 값이라고 할 수 있다. 

 

 

  4. 과대적합 vs 과소적합

 

앞에서 훈련한 모델에 테스트 셋의 점수를 계산해보았다. 그러면 훈련 셋의 점수는 어떻게 될까?

print(knr.score(train_input, train_target)) # 0.9698823289099254

 

훈련 셋으로 모델을 훈련했기 때문에 훈련 셋의 점수가 더 높아야 할 텐데 오히려 테스트 셋 보다 점수가 더 낮게 나온다. 

이처럼 훈련 셋보다 테스트 셋의 점수가 높거나 두 점수가 모두 너무 낮은 경우는 모델이 훈련 셋에 '과소적합(Underfitting)'되었다고 말한다. 

반대로 훈련 셋의 점수보다 테스트 셋의 점수가 지나치게 낮은 경우 모델이 훈련 셋에 '과대적합(Overfitting)'되었다고 말한다. 

 

과소적합 문제는 모델을 조금 더 복잡하게 만들면 된다. 즉, 훈련 셋에 더 잘 맞게 만들면 테스트 점수가 조금 낮아질 것이다.

K-NN에서는 K 값을 줄임으로써 모델을 복잡하게 만들 수 있다. 이웃의 수를 줄이면 훈련 셋에 있는 국지적인 패턴에 민감해지고, 이웃의 개수를 늘리면 데이터 전반에 있는 일반적인 패턴을 따르게 되기 때문이다. 

따라서 K의 값을 5에서 3으로 줄여보자. 

knr.n_neighbors = 3
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target)) # 0.9804899950518966
print(knr.score(test_input, test_target)) # 0.9746459963987609

 

테스트 셋의 점수가 훈련 셋보다 낮아졌고, 둘의 차이도 크지 않기 때문에 모델이 적절하게 만들어졌음을 알 수 있다. 

 

 

  5. K-NN Regression의 한계

 

훈련된 모델을 이용하여 길이가 100인 농어의 무게를 예측해보자. 

print(knr.predict([[100]])) # [1033.33333333]

 

무게를 약 1033g으로 예측하였다. 그런데, 데이터를 참고해보면 길이가 40~44되는 농어의 무게들이 1000 또는 1100인 것을 볼 수 있다. 산점도를 통해 확인해보자. 

 

실제로는 길이가 100인 농어는 훨씬 무게가 많이 나와야 할 것이다. 하지만 가장 가까운 이웃의 길이가 45 근방이기 때문에 이 샘플들의 무게를 이용해 평균을 계산하게 된다. 과장해서 길이가 10000인 농어의 무게도 여전히 1033으로 예측할 것이다. 

이처럼 테스트하고자 하는 샘플에 근접한 훈련 데이터가 없는 경우, 즉 훈련 셋의 범위를 많이 벗어나는 샘플인 경우 정확하게 예측하기 어려운 한계가 있다. 

 

 

참고)
- 혼자 공부하는 머신러닝+딥러닝(박해선, 한빛미디어)

PC로 보시는 것을 권장합니다. 

피드백은 언제나 환영입니다. 댓글로 달아주세요 ^-^

 

 

반응형