[혼공머신] 2주차 03-2 선형회귀
본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.
KNN 알고리즘의 한계
이번에는 다른 알고리즘을 살펴볼 것이다. 하지만, 그냥 다른 알고리즘으로 넘어가기보다는 왜 다른 알고리즘을 써야 하는지 이해하면서 하는 것이 좋을 것이다. 그렇기에 KNN 알고리즘이 가지고 있는 한계와, 그 한계를 극복하기 위해 어떤 알고리즘을 사용해야 하는지 살펴보도록 하자. 먼저, KNN 알고리즘 코드를 작성하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np
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])
X_train, X_test, y_train, y_test = train_test_split(perch_length, perch_weight, random_state=0)
X_train = X_train.reshape(-1, 1)
X_test = X_test.reshape(-1, 1)
knn = KNeighborsRegressor(n_neighbors=3)
knn.fit(X_train, y_train)
knn.predict([[50]])
array([1000.])
우리가 지난번에 살펴보았던 농어 데이터를 학습시켜서 길이가 50cm인 농어의 무게를 예측해 본 것이다. 결과는 1,000g이 나왔다. 그런데 실제 50cm인 농어의 무게는 1.5kg이라고 한다. 어떻게 된 것일까? 한 번 그래프로 살펴보자.
1
2
3
4
5
6
7
8
9
10
import matplotlib.pyplot as plt
distances, indexes = knn.kneighbors([[50]])
plt.scatter(X_train, y_train)
plt.scatter(X_train[indexes], y_train[indexes], marker='D')
plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
우리가 새로 가져온 50cm의 농어는 오른쪽 끝에 혼자 외롭게 동떨어져 있다. 그런데 우리가 예상한 것과 조금 다른 모습인 것을 확인할 수 있다. 이상적인 모양이라면 기존의 데이터대로 오른쪽 위로 더 높은 위치에 있어야 했을 것이다. 그런데 이 농어는 옆으로 그냥 이동해버렸다. 왜 그럴까?
이것은 KNN 알고리즘을 사용했기 때문이다. KNN 알고리즘은 단순히 가장 가까운 점들의 평균으로 값을 추정하기에 기존에 존재하는 점들에 의해 범위가 제한되어 버리는 것이다. 따라서 아무리 길이가 길어도 무게는 기존의 물고기 무게의 최댓값을 벗어날 수 없게 되는 것이다. 그러면 이제 어떻게 해야 할까?
선형 회귀
선형 회귀(Linear regression)은 아마 대부분 들어봤을 것이다. 아마 기초적인 알고리즘으로 배울 텐데, 생각보다 그렇게 호락호락하지 않다. 만약 그렇게 쉬운 거였으면 통계학 전공 과목으로 회귀분석만 1학기를 배우지는 않을 것이다. 하지만 여기서는 매우 간단한 것만 다루기에 그것에 대한 설명은 잠시 접어두고 코드만 살펴보자.
1
2
3
4
5
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(X_train, y_train)
lr.predict([[50]])
array([1228.52162131])
아까 KNN 알고리즘을 사용해서 분석을 했을 때보다 더 원래 농어의 무게에 아까운 값이 나왔음을 알 수 있다. 하지만 그렇게 정확하지는 않다. 우리가 이번에 사용한 것은 단순선형회귀이다. 그냥 X와 y가 linear, 즉 $y=ax+b$와 같이 직선의 관계를 가진다고 가정한 것이다. 이때 기울기 $a$와 절편 $b$는 다음과 같이 속성을 출력해서 확인할 수 있다. 출력된 속성을 바탕으로 관계식을 만들어보면 $y=37.76648694x-659.8027258214022$가 된다.
1
print(lr.coef_, lr.intercept_)
[37.76648694] -659.8027258214022
이제 이것을 바탕으로 그래프를 다시 그려보자.
1
2
3
4
5
6
plt.scatter(X_train, y_train)
plt.plot([15, 50], [15*37.8-659.8, 50*37.8-659.8])
plt.scatter(50, 1228, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
하지만 위의 scatter plot에서는 둘 사이의 관계가 딱히 직선으로 보이지는 않는다. 오히려 곡선에 가까웠다. 그러면 이 곡선 관계를 표현할 수는 없을까?
다항회귀
이러한 문제를 해결하기 위해 이미 수많은 사람들이 연구를 했고, 그에 대한 답도 냈다. 다시 한번 그래프를 살펴보면 곡선을 아래로 볼록한 그래프의 일부분이라고 볼 수도 있을 것이다. 즉, 2차 방정식의 일부로 표현할 수 있지 않을까 생각해 보는 것이다. 그러면 이것을 사용하기 위해 제곱을 한 값을 저장해보자.
1
2
X_train_poly = np.column_stack((X_train**2, X_train))
X_test_poly = np.column_stack((X_test**2, X_test))
그리고 아까 한 것과 같이 이 데이터에 대해서 회귀모형을 학습하고 계수와 절편을 구해보자. 그런데 결과를 보니 이상한 것이 있다. 계수가 2개이다. 분명 우리는 독립변수가 1개였을 텐데 나머지 하나는 어디에 써야 할까? 이번에는 수식으로 한 번 살펴보자.
lr.fit(X_train_poly, y_train)
print(lr.coef_, lr.intercept_)
[ 0.99334411 -20.28904844] 100.69251536566048
우리가 이번에 할 것은 다항회귀(Polynomial Regression)이다.우선, 기본 단순선형회귀 모형이 다음과 같다고 가정해 보자. 그런데, 아까 말했듯이 이러한 모형으로는 비선형적(Non-linear)인 모형은 표현하기에 제한이 된다.
$y_i=\beta_0+\beta_1x_i+\varepsilon_i$
따라서 우리는 기저함수(Basis Function)라는 것을 활용할 것이다. 이것에 대해서 간단하게 설명을 하자면 모델을 설정할 때, $X$를 그대로 쓰는 것이 아니라, 함수로 변형시켜서 사용하는 것이다. 아마 이렇게 말을 하면 잘 이해가 되지 못할 것이다. 따라서 수식으로 보자. 아래를 보니 좀 뭐가 많이 생겼다. 하지만 그렇게 많이 바뀐 것은 아니니 걱정하지 말고 보자.
$y_i=\beta_0+\beta_1b_1(x_i)+\beta_2b_2(x_i)+\cdots+\beta_Kb_K(x_i)+\varepsilon_i$
아까 우리는 train data를 제곱한 데이터를 새로 만들어준 기억이 있을 것이다. 그런데, 기존 데이터를 지우지 않고 같이 사용했었다. 그럼 이것을 식으로 만들면 다음과 같다. 아래 식을 보면 조금 더 확실히 알 수 있을 것이다. 우리가 아까 말한 basis function은 그냥 다른 게 아니고, 원래 $x_i$와 변형한 것들을 말하는 것이다. 여기서는 $b_1(x_i)=x_i, b_2(x_i)=x_i^2$이다.
$y_i=\beta_0+\beta_1b_1(x_i)+\beta_2b_2(x_i)+\varepsilon_i$
$=\beta_0+\beta_1x_i+\beta_2x_i^2+\varepsilon_i$
이렇게 basis function을 통해서 처리를 하는 방법은 polynomial regression 말고도 많이 있으나, 교재에는 나와 있지 않기에 나중에 다루기로 하고, polynomial regression에 대해서만 다루기로 하자. Polynomial regression의 식을 일반화를 하자면, 다음 식처럼 볼 수 있다. 즉, linear regression 모델의 독립변수를 거듭제곱한 항을 추가하는 식으로 기존의 모델을 확장하여 non-linearity를 표현하는 것이다. 이때 참고로 알아두어야 할 것은, polynomial regression을 사용할 때는 overfitting을 방지하기 위해서 $d=3, 4$ 정도로 유지하는 것이 좋다는 것이다.
$y_i=\beta_0+\beta_1x_i+\beta_2x_i^2+\cdots+\beta_dx_i^d+\varepsilon_i$
이제 코드를 살펴보자. 아까 나왔던 결과를 이제는 해석할 수 있을 것이다. 아래와 같이 계수가 두 개가 나왔었는데, 이제는 이 계수가 어디서 튀어나온 것인지 알 수 있다. 아까 새로운 basis function을 왼쪽 열에 추가했기 때문에 두 번째 것은 첫 번째 basis functoin, 즉 $x_i$의 것이고, 첫 번째 것은 $x_i^2$의 계수임을 알 수 있다.
[ 0.99334411 -20.28904844] 100.69251536566048
$y=100.69251536566048+0.99334411x_i^2-20.28904844x_i$
그리고 50cm 농어의 몸무게 예측 결과는다음과 같다.
1
print(lr.predict([[50**2, 50]]))
[1569.60036007]
이제 이것을 바탕으로 그래프를 그리면 다음과 같다. 드디어 우리가 원하던 이쁜 그래프가 나왔다. 하지만 아까보다는 나아졌어도 조금 scatter plot의 추세와 차이가 있어 보이는 것 같다. 이제 다음 게시글에서는 다른 방법을 살펴볼 것이다.
1
2
3
4
5
6
7
8
point = np.arange(15, 50)
plt.scatter(X_train, y_train)
plt.plot(point, 1.0*point**2 - 20.3*point + 100.7)
plt.scatter(50, 1569, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show