본문 바로가기
Programming/Machine Learning

k-최근접 이웃 회귀 사용해보기

by IT learning 2021. 4. 4.
728x90

오늘은 k-최근접 이웃 회귀를 사용해보겠다.

본 내용은 '혼자 공부하는 머신러닝 + 딥러닝' 교재를 이용하여 배운 것을 토대로 작성합니다.

 

위 머신러닝 공부를 할때 사용하는 IDE는 '구글 코랩' 입니다. 

코랩 사용법을 익히고 오시길 바랍니다.

 

또한 파이썬의 기초적인 지식을 알아야 합니다.

위 코드들은 각자의 파일이 아닌 하나의 파일에 작성된 코드들입니다.

 

 

시작하기 전에

저번시간에 우리는 도미와 빙어를 구분하는 모델의 성능을 데이터 전처리를 배움으로써 한차원 업그레이드 시켰었다. 

이 모델이 성공을 이끔에 따라서 다음 프로젝트를 주문해주는 사장님. 

여름 농어 철로 농어 주문이 크게 늘어나자 우리는 업계 최초로 농어를 무게 단위로 판매하려한다. 농어를 마리당 가격으로 판매했을 때 기대보다 볼품없는 농어를 받은 고객이 항의하는 일이 발생했기 때문이다. 

그렇다면 무게단위로 가격을 책정하면 고객들도 합리적이라고 생각하지 않을까? 그런데 공급처에서 생선 무게를 잘못 측정해서 보냈다.

우리는 이 문제를 어떻게 해결할 수 있을까?

 

k-최근접 이웃 회귀

지도 학습 알고리즘은 크게 분류와 회귀로 나뉜다. 분류는 지금까지 계속 해왔던 그 알고리즘이다.

회귀는 클래스 중 하나로 분류하는 것이 아니라 임의의 어떤 숫자를 예측하는 문제이다. 예를 들면 내년도 경제 성장률을 예측하거나 배달이 도착할 시간을 예측하는 것이 회귀 문제이다. 위에서 주어진 농어의 무게를 예측하는 것도 회귀가 된다. 회귀는 정해진 클래스가 없고 임의의 수치를 출력한다.

 

다행히도 전 시간에 배운 k-최근접 이웃 알고리즘이 회귀에도 작동한다. 어떻게 숫자를 예측할 수 있을까? 이 알고리즘의 분류와 회귀에 적용되는 방식을 비교하겠다.

 

k-최근접 이웃 분류 알고리즘은 간단하다. 예측하려는 샘플에 가장 가까운 샘플 k개를 선택한다. 그 다음 이 샘플들의 클래스를 확인하여 다수 클래스를 새로운 샘플의 클래스로 예측한다. 쉽게 말하면 C 주변에 A 가 1개, B 가 2개 일경우 C == B 다 라는 공식을 이용한다는 말이다.

 

k-최근접 이웃 회귀도 간단하다. 분류와 똑같이 예측하려는 샘플에 가장 가까운 샘플 k개를 선택한다. 하지만 회귀이기 때문에 이웃한 샘플의 타깃은 어떤 클래스가 아니라 임의의 수치이다. 이웃 샘플의 수치를 사용해 새로운 샘플 X의 타깃을 예측하는 간단한 방법은 무엇이 있을까? 바로 이 수치들의 평균을 구하면 된다. 얘도 X의 수가 있다면 주변에 100, 80, 60일 경우 X 의 값은 저 숫자들의 평균인 80이 된다.

 

자 그럼 농어 데이터를 준비하고 사이킷런을 사용해 회귀 모델을 훈련하겠다.

 

데이터 준비

먼저 훈련 데이터를 준비 해보겠다. 필자는 농어의 길이만 있어도 무게를 잘 예측할 수 있다고 생각했다.

그럼 농어의 길이가 특성, 무게가 타깃이 될테다. 여기서는 바로 넘파이 배열에서 만들겠다.

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

일단 이 데이터가 어떤 산점도를 띄고 있는지를 알아보자.

import matplotlib.pyplot as plt
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

위 코드의 산점도이다.

농어의 길이가 늘어남에 따라 무게도 같이 늘어나는 것을 볼수가 있다. 이제 농어 데이터를 머신러닝 모델에 사용하기 전에 훈련 세트와 테스트 세트로 나누겠다.

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() 함수를 이용하여 훈련 세트와 테스트 세트를 나누었다. 그리고 필자와 결과가 같게 하기 위해서 random_state를 42로 지정했다.

 

사이킷런에 사용할 훈련 세트는 2차원 배열이어야 한다는 것 기억하는가? perch_length가 1차원 배열이었기 때문에 자연스레 train_input과 test_input 도 1차원 배열이다. 따라서 2차원 배열로 바꾸어야 한다. 다행이 넘파이 배열은 크기를 바꿀 수 있는 reshape() 메소드를 제공한다. 예를 들어 (4, )의 배열을 (2, 2) 크기로 바꿔보자.

# 배열 크기 바꾸기
test_array = np.array([1,2,3,4])
print(test_array.shape)
test_array = test_array.reshape(2,2)
print(test_array.shape)
(4,)
(2, 2)

첫 출력은 1차원 배열로 배열을 넣었기에 (4, ) 배열이 출력됐고, 두 번째 출력은 reshape() 함수를 사용하여 (2, 2)로 변경했기에 적용이 된 모습이다. 

주의 할 점은 새로운 배열을 반환할 때 기존의 배열과 다른 개수로 바꾸려 하면 에러가 발생한다.

test_array = test_array.reshape(2,3)

위와 같은 예시인데, 우리가 변경하려는 배열의 1차원 크기는 (4, ) 이다. 원본 배열의 원소는 4개 인데 2 X 3 = 6개로 바꾸려고 하기 때문에 에러가 나는것이 당연하다. 이 점을 유의해서 reshape() 메소드를 사용하길 바란다.

 

reshape() 메소드를 사용하는 방법은 어렵지 않았다. 이제 이 메소드를 사용해 train_input과 test_input의 배열을 2차원 배열로 변경해보자.

train_input의 크기는 (42, ) 이다. 따라서 2차원 배열로 바꾸고 싶다면, (42, 1)과 같은 형식으로 변경이 가능하다. 하지만, 넘파이는 배열의 크기를 자동으로 지정하는 기능도 존재한다. 따라서 자동으로 지정하는 기능을 사용해보겠다.

크기에 -1을 지정하면 나머지 원소 개수로 모두 채우라는 의미이다.

train_input = train_input.reshape(-1,1) # 크기에 -1을 지정하면 나머지 원소 개수로 모두 채우라는 의미이다.
test_input = test_input.reshape(-1,1)
print(train_input.shape, test_input.shape)

위와 같이 사용하면 2차원 배열로 현재 train_input이 가지고 있는 개수가 자동으로 책정되어 채워지게 된다.

(42, 1) (14, 1)

따라서 이렇게 train_input 과 test_input의 배열이 2차원 배열로 잘 변경이 된것을 볼 수 있다.

 

이제 준비한 훈련 세트를 활용하여 k-최근접 이웃 알고리즘을 훈련 시켜보자.

 

결정계수(R^2)

사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는KNeighborsRegressor이다. 이 클래스의 사용법은 저번 시간에 사용한 것과 비슷하다. 객체를 생성하고 fit() 메소드회귀 모델을 훈련시켜보겠다.

from sklearn.neighbors import KNeighborsRegressor # k-최근접 이웃 회귀 알고리즘
knr = KNeighborsRegressor()
# k-최근접 이웃 회귀 모델을 훈련합니다.
knr.fit(train_input, train_target)

 

후에 점수를 확인해보자.

print(knr.score(test_input, test_target)) 
0.9928094061010639

점수가 괜찮은 듯 싶다! 근데 왜 점수가 이렇게 나오는걸까?

 

분류의 경우는 테스트 세트에 있는 샘플을 정확하게 분류한 개수의 비율이다. 이걸 정확도라고 불렀었다. 간단하게 말하면 정답을 맞힌 개수의 비율인 것이다. 근데 회귀에서는 정확한 숫자를 맞힌다는 것은 거의 불가능하다. 예측하는 값이나 타깃 모두 임의의 수치이기 때문이다.

 

따라서 회귀의 경우에는 조금 다른 값으로 평가하는데 이 점수를 결정계수(coefficient of determination)라고 부른다. 간단히 R^2라고도 부른다. 이름이 조금 어렵지만 계산방식은 간단하다.

(근데 난 수학을 잘 못해서 그냥 쉽게 생각해보자)

만약 타깃의 평균 정도를 예측하는 수준이라면 R^2은 0에 가까워지고, 예측이 타깃에 아주 가까워지면 1에 가까운 값이 되지 않을까?

 

그런것으로 위 결과를 생각해보면 0.99는 정말 좋은 값인듯 싶다. 하지만 정확도처럼 R^2가 직감적으로 얼마나 좋은지 이해하기는 어렵다. 타깃과 예측한 값 사이의 차이를 구해보면 어느정도 예측이 벗어났는지 가늠하기 좋다. 

 

사이킷런은 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)
19.157142857142862

결과에서 예측이 평균적으로 19g 정도 타깃값과 다르다는 것을 알 수 있다. 지금까지는 훈련 세트를 사용해 모델을 훈련하고 테스트 세트로 모델을 평가했었다. 그런데 훈련 세트를 사용해 평가해보면 어떻게 될까? 즉 score() 메소드에 훈련 세트를 전달해서 점수를 출력해 보는것이다. 점수를 출력해보자.

 

과대적합 vs 과소적합

print("훈련 세트 :",knr.score(train_input, train_target))
훈련 세트 : 0.9698823289099255

앞에서 테스트 세트를 사용한 점수와 비교해봐라. 어떤 값이 더 높은가? 훈련 세트에서 나온 점수가 테스트 세트에서 나온 점수보다 현저히 낮지 않은가? 이상하지 않나? 왜 이런 결과가 나온걸까?

 

모델을 훈련 세트에서 훈련하면 훈련 세트에 잘 맞는 모델이 만들어지는게 당연하다. 그럼 이 모델을 훈련 세트와 테스트 세트에서 평가하면 두 값 중 어느 것이 높아야 정상인가? 보통은 훈련 세트에서 테스트 했던게 조금 더 높게 나와야 할 것이다. 

 

만약에 훈련 세트에서 점수가 굉장히 좋았는데 테스트 세트에서 점수가 굉장히 나쁘다면 모델이 훈련 세트에 과대적합(overfitting)되었다고 말한다. 즉 훈련 세트에서만 잘 맞는 모델이라 테스트 세트와 나중에 실전에 투입하여 새로운 샘플에 대한 예측을 만들때 잘 동작하지 않는다는 것이다. 이런 모델은 나 말고도 클라이언트또한 원하지 않는 모델이겠다.

 

반대로 훈련 세트보다 테스트 세트의 점수가 높거나 두 점수가 모두 낮은 경우는 어떨까? 이런 경우를 모델이 훈련 세트에 과소적합(underfitting)되었다고 말한다. 모델이 너무 단순해서 훈련 세트에 적절히 훈련되지 않은 경우이다. 훈련 세트가 젠체 대이터를 대표한다고 가정하기 때문에 훈련 세트를 잘 학습 하는것이 중요하다.

 

위 사항들을 본 결과 우리의 모델의 경우 훈련 세트보다 테스트 세트의 점수가 높으니 과소적합이다. 이 문제를 어떻게 해결할까?

 

모델을 조금 더 복잡하게 만들면 된다. 즉 훈련 세트에 더 잘 맞게 만들면 테스트 세트의 점수는 조금 낮아질 것이다. k-최근접 이웃 알고리즘으로 모델을 더 복잡하게 만드는 방법은 이웃의 개수 k를 줄이는 것이다. 이웃의 개수를 줄이면 훈련 세트에 있는 국지적인 패턴에 민감해지고, 이웃의 개수를 늘리면 데이터 전반에 있는 일반적인 패턴을 따를 것이다. 여기에서 사이킷런의 k-최근접 이웃 알고리즘의 기본  k값은 5이다. 이를 3으로 낮춰보자.

knr.n_neighbors = 3

# 모델을 다시 훈련시킵니다.
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target))

k값을 바꾸는 방법은 저번시간에도 알려주었지만, n_neighbors 속성값을 변경하면 된다.

k값을 변경하고 다시 훈련시켜 점수를 보았다.

0.9804899950518966

k값을 줄였더니 훈련 세트의 R^2 점수가 높아졌다. 이제 테스트 세트의 점수를 보자.

print(knr.score(test_input, test_target))
0.974645996398761

예상대로 테스트 세트의 점수는 훈련 세트보다 낮아졌으므로 과소적합 문제를 해결한 듯 싶다. 또 두 수의 차이가 그렇게 큰 것도 아니니 과대적합이 된것도 아닌듯 하다.

 

성공적으로 회귀 모델을 훈련해봤다.

 

회귀 문제 다루기 정리

사장님은 우리에게 농어의 높이, 길이 등의 수치로 무게를 예측해 달라고 요청했다. 이 문제는 분류의 문제가 아니라 회귀의 문제이다.

회귀는 임의의 수치를 예측하는 문제이다. 우리는 농어의 길이를 사용해 무게를 예측하는 k-최근접 이웃 회귀 모델을 만들었다.

 

k-최근접 이웃 회귀 모델은 분류와 동일하게 가장 먼저 가까운 k개의 이웃을 찾는다. 그 다음 이웃 샘플의 타깃값을 평균하여 이 샘플의 예측값으로 사용한다.

 

사이킷런은 회귀 모델의 점수로 R^2, 즉 결정계수 값을 반환한다. 이 값은 1에 가까울수록 좋다. 절대값의 오차를 구하고 싶을 경우 사이킷런에서 제공하는 다른 평가 도구들을 사용할 수 있었다.

 

모델을 훈련하고 나서 훈련 세트와 테스트 세트에 대해 모두 평가 점수를 구할 수 있었다. 훈련 세트의 점수와 테스트 세트의 점수 차이가 크면 좋지 않다. 일반적으로 훈련 세트의 점수가 테스트 세트의 점수보다 조금 더 높다. 만약에 테스트 세트의 점수가 너무 낮다면 모델이 훈련 세트에 과도하게 맞춰진 것이다. 이것을 우리는 과대적합이라고 부른다. 반대로 테스트 세트의 점수가 너무 높거나 두 점수 모두 낮을 경우 과소적합이다.

 

과대적합일 경우 모델을 덜 복잡하게 만들어야 한다. 과소적합일 경우 모델을 더 복잡하게 만들어야 한다. k-최근접 이웃의 경우 복잡의 정도는 k값을 기준으로 나누어지고, k값의 기본은 5이다.

 

github.com/ITlearning/Practice_Machine_Learning/blob/main/2021_04/04_02/Regression.ipynb

 

ITlearning/Practice_Machine_Learning

사지방에서 공부하는 머신러닝 + 딥러닝. Contribute to ITlearning/Practice_Machine_Learning development by creating an account on GitHub.

github.com

이번 파트 코드이다.

728x90

댓글

IT_learning's Commit