K-Nearest Neighbors Regression

변현섭·2024년 7월 2일
0
post-thumbnail

1. 개념

머신 러닝 알고리즘은 지도학습과 비지도학습, 강화학습으로 구분된다. 또한, 지도학습은 다시 Calssification(분류)과 Regression(회귀)으로 나뉜다. 이미 지난 포스팅에서 분류에 대해 알아보았으므로, 이번 포스팅에선 회귀에 대해 알아보기로 하자.

먼저 회귀란, 여러 개의 독립 변수가 종속 변수에 미치는 영향을 분석하여 특정 수치를 예측하는 기법을 말한다. 예를 들어, 버스와 버스 정류장 사이의 거리를 기준으로 버스 도착 시간을 예측하는 것 등이 회귀 분석에 해당한다.

회귀 분석에는 다양한 알고리즘이 사용될 수 있지만, 여기서는 KNN 알고리즘을 사용할 것이다. KNN 알고리즘을 통해 새로운 Sample X의 타깃 값을 예측하는 과정은 아래와 같다.

  • KNN Calssification과 동일하게 먼저 K개의 최근접 이웃을 선택한다.
  • Calssification에서 선택된 Sample의 Class를 확인한 것과 달리, Regression에서는 Sample의 수치 데이터를 확인한다.
  • 확인한 수치의 평균으로 X의 타깃 값을 예측한다.

2. 두 변수 사이의 상관 관계 분석하기

지금부터 농어의 길이 데이터를 이용하여 무게를 예측하는 모델을 만들어볼 것이다. 모델을 설계하기에 앞서 이러한 모델이 타당한지를 평가하는 과정이 선행되어야 한다. 따라서 먼저는, 길이와 무게 사이의 상관 관계를 분석해보기로 하자.

import numpy as np

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])

위에 주어진 데이터의 규칙성을 파악해보기 위해 Scatter Flot을 그려보자.

import matplotlib.pyplot as plt
...

plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('width')
plt.show()

길이와 무게가 어느 정도 비례 관계에 놓여있는 모습이다. 즉, 길이를 통해 무게를 예측하는 모델이 충분히 타당할 것으로 판단된다.

3. 훈련 Set과 테스트 Set 만들기

1) Sample 랜덤 선택

이번에도 train_test_split() 메서드를 활용하여 훈련 Set과 테스트 Set을 구분할 것이다. 한 가지 주의해야 할 것은, 길이를 통해 무게를 예측할 것이므로, 여기서의 무게는 Feature가 아닌 Target이라는 것이다.

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)

2) 배열의 차원 변환

여기서 한 가지 문제가 발생한다. KNeighborsClassifier의 fit() 메서드는 훈련 데이터가 2차원 배열일 것이라 기대하는데, 위 코드에서 train_input은 1차원 배열이기 때문이다. 저번 시간에는 Feature가 2개였기 때문에, 자연스레 train_input도 2차원 배열이 되었지만, 이번에는 Feature가 1개이기 때문에 직접 2차원 배열로 변환해야 한다.

배열의 차원 변환을 위해선, Numpy에서 제공하는 reshape() 메서드를 사용할 수 있다. 현재 train_input의 크기는 (42, ) 이다. 즉, train_input을 2차원 배열로 변환하는 코드를 아래와 같이 작성할 수 있다.

train_input.reshape(42, 1)

하지만, 배열의 크기를 알아야 한다는 점에서 다소 불편하게 느껴진다. 이 때, 배열의 크기 대신 -1을 전달하여, 배열의 크기를 자동으로 지정할 수 있다.

train_input.reshape(-1, 1) # train_input.reshape(42, 1)과 동일

train_input과 test_input을 2차원 배열로 변환하면, 훈련 Set과 테스트 Set 준비가 완료된다.

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

4. K-Nearest Neighbors 회귀 모델 만들기

1) 회귀 모델 훈련

사이킷런에서 KNN 회귀를 구현한 클래스는 KNeighborsRegressor이다. 그러나 import 해야 하는 모듈만 다를 뿐, 사용법은 KNeighborsClassifier와 거의 동일하다.

from sklearn.neighbors import KNeighborsRegressor
...

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

훈련이 완료되었으니, 이제 모델의 성능을 평가해보자.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsRegressor

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])

train_input, test_input, train_target, test_target = train_test_split(perch_length, perch_weight, random_state=42)
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)
knr = KNeighborsRegressor()
knr.fit(train_input, train_target)
knr.score(test_input, test_target)

1에 가까운 것으로 보아 좋은 점수인 것은 분명하다. 그런데, 이 수치는 어떻게 계산된 것일까? 정확하게 맞고 틀리고를 구분할 수 있는 Classification과는 달리, Regression에서는 정확히 맞는다는 개념이 존재하지 않는다. 다시 말해, 회귀 모델의 성능을 평가하기 위한 새로운 기준이 필요하다는 것이다. 바로 이 때, 사용되는 기준을 결정계수(Coefficient of Determination) 또는 R²(R-Squared)라 부른다.

2) 결정계수

결정계수는 회귀 분석에서 모델의 적합도를 평가하는 지표로, 회귀 모델이 종속 변수의 변동성(Variance)을 얼마나 잘 설명하는지를 나타낸 값이다. 결정 계수를 계산하는 방법은 아래와 같다.

여기서, 오차는 예측 값과 실제 값 사이의 차이를 의미하며, 편차는 예측 값과 평균 값 사이의 차이를 의미한다. 그렇다면, R²의 의미는 무엇일까?

R²의 값은 모델이 평균 정도를 예측할 때, 0에 수렴한다. 쉽게 말해, 농어의 길이를 이용해 무게를 예측하는 모델이 항상 평균 무게와 비슷한 수치를 예측한다면, 이 모델은 무의미한 예측을 하고 있는 것이다. (길이라는 별도의 독립 변수를 사용하였으나, 새로운 결과를 도출해내지 못했기 때문.) 이러한 이유에서, 회귀 모델의 성능 평가에 결정 계수가 사용되는 것이다.

조금 더 구체적인 예를 살펴보자. 일례로, 키를 기준으로 사람의 체중을 예측하는 상황을 가정하자. 사람들의 키와 몸무게에 대한 데이터를 수집한 후, 회귀 분석을 수행했더니 R² 값이 0.8이 나왔다. 이는 체중 변동성의 80% 정도를 키로 설명할 수 있다는 의미이다. (나머지 20%는 식단, 운동, 유전 등과 같이 모델에 포함되지 않은 다른 요인으로 설명될 수 있다.) 즉, 키라는 독립 변수가 체중이라는 종속 변수의 변동성을 잘 설명할 수 있다는 의미가 된다.

이번에는 광고 비용을 기준으로 제품 판매 수량을 예측해보자. 마찬가지로, 데이터 수집 후 회귀 분석을 수행했더니 R² 값이 0.25가 나왔다. 이는 판매 수량의 25% 정도를 광고 비용으로 설명할 수 있다는 의미이다. 즉, 광고 비용과 판매 수량 사이에는 어느 정도 연관성이 있으나, 그 관계가 매우 약하다는 의미이다. 따라서, 광고 비용이라는 독립 변수는 판매 수량이라는 종속 변수의 변동성을 충분히 설명하지 못하고 있다. 이와 같이 회귀 모델이 데이터의 변동성을 잘 설명하지 못할 때에는, 추가적인 독립 변수를 고려해보아야 한다.

3) Mean Absolute Error

결정계수는 모델의 성능을 평가하기에 충분히 좋은 지표이지만, 예측 값과 실제 값의 오차를 알아내기에는 부적합하다. 그러므로 예측 값과 실제 타깃 값 사이의 평균적인 오차를 계산해야 할 때에는 MAE(Mean Absolute Error, 평균 절대 오차)를 사용해야 한다. MAE는 회귀 모델의 예측 성능을 평가하는 보조 지표로, sklearn.metrics 패키지의 mean_absolute_error 메서드를 사용하여 쉽게 구할 수 있다.

from sklearn.metrics import mean_absolute_error
...

test_prediction = knr.predict(test_input)
mae = mean_absolute_error(test_target, test_prediction)
print(mae)

모델의 예측 값과 실제 정답 간의 평균적인 오차는 약 19g 정도이다. 19g 정도의 오차가 큰 오차인지 작은 오차인지는 문제의 특성마다 조금씩 다를 수 있다. 여기서는 대부분의 농어가 100g을 크게 상회하기 때문에, 충분히 작은 오차로 간주할 수 있을 것 같다.

5. Overfitting과 Underfitting

1) 개념

지금까지 훈련 Set을 이용해 모델을 훈련하였고, 테스트 Set을 이용해 모델의 성능을 평가해보았다. 그런데 만약, 훈련 Set을 이용하여 모델의 성능을 평가하면 어떻게 될까? 훈련 Set과 테스트 Set의 데이터가 다르기 때문에, 결정 계수가 조금은 다르게 계산될 것이다.

knr.fit(train_input, train_target)
knr.score(train_input, train_target)

이렇듯 훈련 Set과 테스트 Set에서 평가되는 모델의 성능에는 약간의 차이가 존재하게 된다. 다행히 여기에서는 성능 차이가 크게 나타나지 않았지만, 간혹 성능 차이가 극단적으로 나타나는 경우도 존재하는데, 이러한 경우를 Overfitting(과대 적합) 또는 Underfitting(과소 적합)이 발생했다고 한다.

먼저 과대 적합은 훈련 Set에서 성능이 아주 높게 평가된 모델이 테스트 Set에서 성능이 아주 낮게 평가되는 경우를 말한다. 당연히 실전에 투입되었을 때, 높은 수준의 예측 성능을 기대하기 어렵다.

반대로, 과소 적합은 훈련 Set에서 성능이 아주 낮게 평가되었다가 테스트 Set에서 성능이 아주 높게 평가되는 경우를 말한다. 이는 훈련 Set이 일반적인 데이터들을 충분히 대표하지 못하고 있다는 의미이므로, 모델이 적절하게 훈련되었다고 평가하기 어렵다.

모델은 훈련 Set으로 훈련되기 때문에, 일반적으로 과소 적합보다는 과대 적합이 더 빈번하게 발생된다. 어찌됐건, 과대 적합과 과소 적합 모두 피해야 하는 문제이기 때문에 이에 대한 해결 방법을 알고 있어야 한다.

2) 해결 방법

과대 적합 또는 과소 적합을 해결하는 아이디어는 생각보다 간단하다. 단순히 균형을 맞추면 된다. 우리가 만든 모델은 훈련 Set에서의 점수보다 테스트 Set에서의 점수가 더 높다(과소 적합). 따라서, 훈련 Set에서의 점수는 높이고, 테스트 Set에서의 점수는 조금 낮추어 균형을 맞추어야 한다.

KNN 알고리즘에 사용되는 K 값을 줄이면, 훈련 Set의 일부 데이터 포인트에 민감하게 반응하게 되고, 반대로 K 값을 늘리면 전반적인 패턴을 따르게 된다. 즉, KNN에서 과소 적합이 발생했다면, 훈련 Set의 각 데이터 포인트의 영향력을 높이기 위해 K 값을 줄여야 한다. 이렇게 하면 훈련 Set에 조금 더 특화된 모델이 완성되기 때문에 훈련 Set에서의 점수가 높아짐과 동시에, 테스트 Set에서의 점수는 조금 낮아진다.

knr = KNeighborsRegressor(n_neighbors = 3) # K 값을 3으로 설정
knr.fit(train_input, train_target)
knr.score(train_input, train_target)

K 값을 줄였더니 훈련 Set에서의 R² 값이 더 높아졌다. 계속해서 테스트 Set에서의 R² 값도 확인해보자.

knr.score(test_input, test_target)

이로써, 과소 적합 문제가 해결되었다. 또한, 두 점수가 큰 차이를 보이지 않기 때문에, 과대 적합도 발생하지 않은 것으로 볼 수 있다.

profile
LG전자 Connected Service 1 Unit 연구원 변현섭입니다.

0개의 댓글