Machine learning with Python (5)

K-최근접 이웃의 한계

Input

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])
       
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_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

from sklearn.neighbors import KNeighborsRegressor

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

최근접 이웃 개수를 3으로 하는 모델을 훈련한다.

Input

# 예측하고 싶은 데이터
print(knr.predict([[50]]))

Output

[1033.33333333]

나의 모델은 50cm perch의 무게를 1,033g 정도로 예측했다. 그런데 실제 이 perch의 무게는 훨씬 더 많이 나간다고 한다. 어디서 문제가 생겼을까?

훈련 세트와 50cm의 perch 그리고 이 perch의 최근접 이웃을 산점도로 표시하겠다. kneighbors() 메서드를 사용하면 가장 가까운 이웃까지의 거리와 이웃 샘플의 인덱스를 얻을 수 있다.

Input

import matplotlib.pyplot as plt

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

Output

길이가 50cm이고 무게가 1,033g인 perch는 삼각형(marker='^') 으로 표시되고 그 주변의 샘플은 마름모(marker='D')이다. 그림으르 그려보니 문제가 드러난다. 이 산점도를 보면 길이가 커질수록 perch의 무게가 증가하는 경향이 있다. 하지만 50cm perch에서 가장 가까운 것은 45cm 근방이기 때문에 k-최근접 이웃 알고리즘은 이 샘플들의 무게를 평균한다. 이웃 샘플의 타깃 평균을 구해보면

Input

print(np.mean(train_target[indexes]))

Output

1033.3333333333333

모델이 예측했던 값과 정확히 일치하는 것을 볼 수 있다. k-최근접 이웃 회귀는 가장 가까운 샘플을 찾아 타깃을 평균한다. 따라서 새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측할 수 있다. 예를 들어 길이가 1000cm인 perch도 여전히 1,033g으로 예측할 것이다.

Input

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

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

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

Output

이런 식이면 perch가 아무리 커도 무게가 늘어나지 않는다.

k-최근접 이웃을 사용해 이 문제를 해결하려면 가장 큰 perch가 포함되도록 훈련 세트를 다시 만들어야 한다. 난 이런 방법이 마음에 들지 않는다.............


선형 회귀

선형 회귀linear regression는 널리 사용되는 대표적인 회귀 알고리즘이다. 비교적 간단하고 성능이 뛰어나기 때문에 맨 처음 배우는 머신러닝 알고리즘이다.

사이킷런은 sklearn.linearmodel 패키지 아래에
LinearRegressoin_ 클래스로 선형 회귀 알고리즘을 구현해 놓았다. 이 클래스의 객체를 만들어 훈련하겠다.

Input

from sklearn.linear_model import LinearRegression
lr = LinearRegression()

lr.fit(train_input, train_target)
print(lr.predict([[50]]))

Output

[1241.83860323]

k-최근접 이웃 회귀를 사용했을 때와 달리 선형 회귀는 50cm perch의 무게를 아주 높게 예측했다. 선형 회귀는 말 그대로 직선을 임의의 직선을 그려보고 그 직선을 기준으로 예측하는 알고리즘이다.

하나의 직선을 그리려면 기울기와 절편이 있어야 한다. y = a x b처럼 쓸 수 있다.여기서 x를 길이, y를 무게로 바꾸면
perch's weight = a * perch's length + b
a = coef
b = slope

어릴 때 배웠던 간단한 직선의 방정식이다.
Linearregression 클래스는 이 데이터에 가장 잘 맞는 a와 b를 찾았을 것이다. lr객체의 coef_와 intercept_ 속성에 저장되어 있다.

Input

print(lr.coef_, lr.intercept_)
# coef = 기울기  intercept = 절편 - 축과 만나는 값(Y)

Output

[39.01714496] -709.0186449535477

coef_와 intercept_를 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터model parameter라고 부른다.

perch의 길이 15에서 50까지 직선으로 그려보자. 직선을 그리려면 앞에서 구한 기울기와 절편을 사용하여 (15, 15 * 39 - 709)(50, 50 * 39 - 709) 두 점을 이으면 된다. 훈련 세트의 산점도와 함께 그려보겠다

Input

plt.scatter(train_input, train_target)

# perch's weight = coef * length + intercept
plt.plot([15, 50], [15 * lr.coef_ + lr.intercept_, 50 * lr.coef_ + lr.intercept_])

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

Output

바로 이 직선이 선형 회귀 알고리즘이 이 데이터셋에서 찾은 최적의 직선이다. 길이가 50cm인 perch에 대한 예측은 이 직선의 연장선에 있다. 이제 훈련 세트 범위를 벗어난 perch의 무게도 예측할 수 있겠지? 그럼 훈련 세트와 테스트 세트에 대한 R^2점수를 확인하겠다

Input

print(lr.score(train_input, train_target))
print(lr.score(test_input, test_target))

Output

0.9398463339976039
0.8247503123313558

훈련 세트와 테스트 세트의 점수 차이가 조금 난다. 이 모델은 전체적으로 과소적합되었다고 볼 수 있다. 사실 훈련 세트의 점수도 높지 않다. 과소적합 말고도 다른 문제가 또 있다.


다항 회귀

선형 회귀가 만든 직선이 왼쪽 아래로 쭉 뻗어 있다. 이 직선대로 예측하면 perch의 무게가 0g 이하로 내려갈 텐데 무게가 음수로 나온다는 건 말도 안되는 일이다.

산점도를 보고 알 수 있듯이 perch의 길이와 무게의 산점도는 일직선이라기보단 왼족 위로 조금 구부러진 곡선에 가깝다. 그럼 직선을 찾기보다 곡선을 찾으면 어떨까?

이런 2차 방정식의 그래프를 그리려면 길이를 제곱한 항이 훈련 세트에 추가되어야 한다. 사실 넘파이를 사용하면 아주 간단히 만들 수 있다. 다음처럼 perch의 길이를 제곱해서 원래 데이터 앞에 붙여보겠다. column_stack() 함수를 사용하면 간단하다.

Input

# x^2  x <- insert
train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))

train_input ** 2 식에도 넘파이 브로드캐스팅이 적용된다. 즉 train_input에 있는 모든 원소를 제곱한다.

Input

print(train_poly.shape, test_poly.shape)

Output

(42, 2) (14, 2)

이제 train_poly를 사용해 선형 회귀 모델을 다시 훈련하겠다. 2차 방정식 그래프를 찾기 위해 훈련 세트에 제곱 항을 추가했지만, 타깃값은 그대로 사용한다. 목표하는 값은 어떤 그래프를 훈련하든지 바꿀 필요가 없다. 이 훈련 세트로 선형 회귀 모델을 훈련한 다음 50cn짜리 perch에 대해 무게를 예측해보자. 테스트할 때는 이 모델의 perch 길이의 제곱과 원래 길이를 함께 넣어 주어야 한다.

Input

lr = LinearRegression()
lr.fit(train_poly, train_target)

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

Output

[1573.98423528]

앞서 예측했던 값보다 더 높은 값을 예측했다. 이 모델이 훈련한 계수와 절편을 출력해 보면

Input

print(lr.coef_, lr.intercept_)
# ax^2 + bx + c
# a = 기울기
# b = 대칭축 결정
# c = y 절편
# y = (1.01 * x^2) + (-21.6 * x) + 116.05
#          x^2 = train_poly[:, 0]
#            x = train_poly[:, 1]

Output

[  1.01433211 -21.55792498] 116.05021078278276

아 참고로 주석들은 무시해도 된다.
이 모델은 다음과 같은 그래프를 학습했다.

weight = 1.01 * (length ^ 2) - 21.6 * length + 116.05

이런 방정식을 다항식polynomial이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀polynomial regression라 부른다. 이 2차 방정식의 계수와 절편 a, b, c를 알았으니 이전과 동일하게 훈련 세트의 산점도에 그래프를 그려 보겠다. 짧은 직선을 이어서 그리면 곡선처럼 표현할 수 있다.

Input

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

Output

np.arange()를 이용해서 1씩 짧게 끊어 직선을 곡선처럼 보이게 그렸다. 단순 선형 회귀 모델보다 훨씬 나은 그래프가 그려진 것 같다. 이제 무게가 음수로 나올 일은 없을 것이다. R^2점수를 평가하겠다.

Input

print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))

Output

0.9706807451768623
0.9775935108325122

훈련 세트와 테스트 세트에 대한 점수가 크게 높아졌다. 하지만 여전히 테스트 세트의 점수가 조금 더 높다. 과소적합이 남아 있는 것 같다...........................

좋은 웹페이지 즐겨찾기