티스토리 뷰

주어진 데이터를 가지고 학습하여 테스트 데이터를 "분류"하는 k-최근접 이웃 알고리즘에 대하여 직전 포스팅에서 다뤘다. 물론 k-최근접 이웃 알고리즘으로도 특정 값을 예측하는 "회귀" 모델을 만들 수 있지만, 그 한계가 명확하다.

 

아래 예제로 한번 k-최근접 이웃 알고리즘의 회귀 모델이 갖는 한계점을 확인해보자.


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

위 데이터는 농어의 길이와 무게 데이터이다. 이 데이터를 가지고 k-최근접 이웃 알고리즘에 대입하여 농어의 길이에 따른 무게를 예측하는 모델을 만들려고 하면, 우선 길이, 무게 데이터가 1차 배열이므로 reshape하여 크기가 3, 1인 1차원 배열로 만들어 준 뒤 아래와 같이 코드를 짜면 된다.

 

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor

import numpy as np
import matplotlib.pyplot as plt

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)

# k-최근접 이웃 회귀
knr = KNeighborsRegressor()

knr.n_neighbors = 3

knr.fit(train_input, train_target)

print(knr.predict([[50]]))

위 코드의 결과 값은 [1033.33333333] 이라는 값이 나온다.

 

44cm 인 농어가 1000g 이라는 데이터가 있는데, 6cm나 차이나는 농어의 무게가 거의 같다는 것이 말이 안된다.

 

데이터와 길이가 예측 값, 이웃 값을 좌표평면에 찍어보면 바로 원인을 찾을 수 있다.

 

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression

import numpy as np
import matplotlib.pyplot as plt

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.n_neighbors = 3

knr.fit(train_input, train_target)

distances, indexes = knr.kneighbors([[50]])

plt.scatter(train_input, train_target)

plt.scatter(train_input[indexes], train_target[indexes], marker='D')

plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')

plt.show()

 

초록색 데이터가 길이가 50cm 인 농어의 예측 무게 데이터, 주황색 세모가 최근접 3개의 이웃이다.

 

농어의 길이가 아무리 길어져도 해당 데이터의 최근접 데이터 셋에는 변화가 없기 때문에, 길이가 1000cm인 농어의 데이터를 입력해도 그 결과는 1000g에서 큰 차이가 없는 것이다. 

 

즉, k-최근접 이웃 알고리즘은 학습한 데이터 범위가 벗어나버리면 그 예측값의 정확도가 크게 떨어진다는 것이다.

 

 

따라서, 우리는 선형 회귀를 사용해야 한다.

 

선형회귀는 그 이름에서 짐작 가능하듯, 특성이 하나인 경우에 데이터의 패턴대로 직선의 방정식을 도출하고, 그 직선을 토대로 예측 값을 구하는 알고리즘이다. 

 

바로 확인해보자.


from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression

import numpy as np
import matplotlib.pyplot as plt

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)

# 선형 회귀
lr = LinearRegression()

# 선형 회귀 모델을 학습
lr.fit(train_input, train_target)

# 50cm 농어에 대한 예측값
print(lr.predict([[50]]))

# 데이터 모델로 부터 도출한 직선의 기울기(가중치)와 절편
print(lr.coef_, lr.intercept_)

# 훈련 데이터의 산점도
plt.scatter(train_input, train_target)

# 15 ~ 50 까지의 1차 방정식 그래프
plt.plot([15, 50], [15*lr.coef_ + lr.intercept_, 50*lr.coef_ + lr.intercept_])

plt.scatter(50, lr.predict([[50]]), marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

선형 회귀 모델의 50cm 길이의 농어의 무게가 1241.83860323 정도라고 예측한다. 이는 k-최근접 이웃 알고리즘보다 훨씬 신빙성 있는 예측값이다. 

 

하지만, 해당 선형 회귀 모델에 대한 정확도 Score를 확인해보면,

 

훈련 데이터 >> 0.939846333997604

테스트실 데이터 >> 0.824750312331356

 

정도로 전체적으로 선형 회귀 모델의 대한 R제곱 Score가 낮다는 것을 확인할 수 있다.

 

이는 선형 그래프와 데이터 산점도를 확인해보면 금방 눈치챌 수 있다.

보면 애초에 모델이 되는 데이터가 선형적인 형태와 맞지 않다. 현실에서는 길이가 0, 무게가 0인 농어는 존재하지 않기 때문이다. 데이터를 잘 살펴보면, 일직선이라기 보단, 왼쪽 위로 조금 구부러진 곡선에 가깝다.

 

곡선인 2차 방정식의 그래프를 그리려면 길이를 제곱한 항이 훈련 데이터 세트에 추가되어야 한다. 1차원  벡터인 train, test input 데이터에 각 항을 제곱한 데이터를 아래와 같이 붙여주자.

 

train_poly = np.column_stack((train_input**2, train_input))
test_poly = np.column_stack((test_input**2, test_input))

동일하게 해당 데이터로 학습을 시켜준 뒤, 예측 값을 확인해보면 [1573.98423528] 로 직선을 사용하여 도출했을 때 보다 더 큰 값을 예측했다. 

 

해당 모델의 가중치와 절편은 [  1.01433211 -21.55792498] 116.0502107827827 로 확인되기 때문에, 아래와 같이 2차 방정식 그래프와 함께 산점도를 확인하면

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression

import numpy as np
import matplotlib.pyplot as plt

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)

# 선형 회귀
lr = LinearRegression()

train_poly = np.column_stack((train_input**2, train_input))
test_poly = np.column_stack((test_input**2, test_input))

lr.fit(train_poly, train_target)

print(lr.predict([[50**2, 50]]))

print(lr.coef_, lr.intercept_)

point = np.arange(15, 50)

plt.scatter(train_input, train_target)

plt.plot(point, 1.01*point**2 - 21.6*point + 116.05)

plt.scatter(50, 1574, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

위와 같이 이전과 다르게 그래프와 산점도가 매우 높은 수준으로 맞춰진다. 

 

이런 식으로 단항, 다항 선형 회귀를 사용하여 훈련 데이터 셋에 없는 값을 예측하는 모델을 만들 수 있다.


끝!

Comments