머신러닝 알고리즘에는 크게 지도학습과 비지도학습이 있다고 했다. 이 중, 지도학습은 크게 회귀와 분류로 나뉘는데, 저번에 다루었던 도미인지 빙어인지를 맞추는 것이 분류이고, 어떤 임의의 수치를 예측하는 것이 '회귀' 이다. 오늘은 이 회귀에 대해서 자세히 알아보겠다.
앞서 k-최근접 이웃 알고리즘으로 분류를 다루었다. 이번에는 동일한 알고리즘으로 회귀를 다뤄보겠다. 들어가기에 앞서 간단한 알고리즘 동작 원리에 대해 설명하자면, 회귀에서 예측하는 것은 임의의 수치이기 때문에, 타깃값을 예측할 때에는 인접값들의 평균을 구하는 방법을 사용한다.
먼저 훈련 데이터를 준비해 보자.
데이터는 https://gist.github.com/rickiepark/2cd82455e985001542047d7d55d50630 에서 가져왔다.
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차원 배열이어야 한다. 이런 1차원 배열을 1개의 열이 있는 2차원 배열로 바꿔줘야 한다. 그럼 이 배열을 바꿔주는 과정을 한번 보자.
test_array = np.array([1,2,3,4])
print(test_array.shape)
(4,)
test_array가 (4,)인 것을 확인했으니, 이를 다시 (2,2)크기로 바꿔보자.
test_array = test_array.reshape(2,2)
print(test_array.shape)
(2,2)
reshape() 메서드를 통해 배열의 크기를 바꿔줄 수 있는 것을 확인했다. 하지만 여기서 주의할 점은 지정한 크기와 원본 배열의 원소 개수가 달라지면 ValueError가 나타나게 된다.
이제 train_input과 test_input의 배열도 바꿔보자.
train_input = train_input.reshape(-1,1)
test_input = test_input.reshape(-1,1)
print(train_input.shape, test_input.shape)
(42, 1) (14, 1)
여기서 크기를 지정할 때, -1의 값을 썼다. 이는 넘파이의 기능 중 하나이다. 넘파이는 배열의 크기를 자동으로 지정하는 기능을 제공하기에 우리가 크기를 일일이 다 기억하지 않아도 이렇게 입력하면 자동으로 지정해준다. 즉 매번 전체 원소의 개수를 외우지 않아도 된다는 것이다. 정말 편리하다.
사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 KNeighborsRegressor이다. 이 클래스의 사용법은 앞서 배운 KNeighborsClassifier와 비슷하다. 객체를 생성하고 fit() 메서드로 회귀 모델을 훈련해 보겠다.
from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor()
# k-최근접 이웃 회귀 모델을 훈련
knr.fit(train_input, train_target)
print(knr.score(test_input, test_target))
0.992809406101064
코드의 결과값으로 나온 이 점수가 결정계수이다. 분류의 경우에는 이 점수가 테스트 세트에 있는 샘플을 정확하게 분류한 개수의 비율, 정확도 라고 했다. 간단히 말하면 정답을 맞힌 개수의 비율이 점수였다. 하지만 회귀에서는 정확한 숫자를 맞힌다는 것은 거의 불가능하다. 회귀는 말그대로 어떠한 수치를 예측하는 방법이기 때문이다. 즉 예측값과 타깃값 모두 임의의 수치이기에 정확하게 그것을 맞춘다는 것은 불가능에 가깝다.
그렇기에 회귀에서는 이 점수를 다른 방법으로 평가하는데 그것을 '결정계수' 라고 한다. 결정계수를 구하는 방법은 다음과 같다.
결정계수는 값이 높을수록 좋은 값이지만, 직감적으로 얼마나 좋은지 바로 이해하기는 어렵다. 좀 더 직감적으로 얼마나 좋은 값인지를 알아보기 위해서는 타깃과 예측의 절댓값 오차의 평균을 구해보는 방법이 있다. 이를 통해서 우리는 결과에서 예측이 평균적으로 얼마가 차이나는지를 알 수 있다.
사이킷런은 sklearn.metrics 패키지 아래 여러가지 측정 도구를 제공한다. 이 중에서 mean_absolute_error가 타깃과 예측의 절댓값 오차의 평균을 구해주는 도구이다.
그럼 이를 통해 얼마나 차이나는지를 알아보자.
from sklearn.metrics import mean_absolute_error
#test 세트에 대한 예측 만들기
test_prediction = knr.predict(test_input)
#test 세트에 대한 절댓값 오차의 평균 계산하기
mae = mean_absolute_error(test_target, test_prediction)
print(mae)
19.157142857142862
이를 통해 결과에서 예측이 평균적으로 19g정도 타깃값과 다르다는 것을 알 수 있다.
지금까지는 훈련 세트를 사용해 모델을 훈련하고 테스트 세트로 모델을 평가했다. 그럼 이제 훈련 세트를 사용해 평가를 해보자.
print(knr.score(train_input, train_target))
0.9698823289099254
앞의 결과값이 좀 더 높은 점수를 가지는 것을 볼 수 있다.
모델을 훈련 세트에 훈련하면 훈련 세트와 잘 맞는 모델이 만들어진다. 모델이 훈련세트로 훈련했기 때문에, 훈련세트와 테스트세트에 대해 평가하면 훈련세트의 점수가 더 높게 나오는 것은 당연하다.
만약, 훈련 세트에서 점수가 굉장히 좋았는데 테스트 세트에서는 점수가 나쁘면 모델이 훈련 세트에 과대적합(overfitting) 되었다고 말한다. 즉 훈련 세트에만 잘 맞는 모델이라 테스트 세트와 나중에 실전에 투입하여 새로운 샘플에 대한 예측을 만들 때 잘 동작하지 않을 수 있다는 것이다.
반대로 훈련 세트보다 테스트 세트의 점수가 높거나 두 점수가 모두 너무 낮은 경우는 모델이 훈련 세트에 과소적합(underfitting) 되었다고 한다. 즉 모델이 너무 단순해 훈련 세트에 적절히 훈련되지 않은 경우이다. 훈련 세트가 데이터를 대표한다고 가정하기에 훈련 세트를 잘 학습하는 것이 중요하다.
그럼 우리가 평가한 훈련 세트와 테스트 세트는 어떨까? 훈련 세트보다 테스트 세트의 점수가 높기에 과소적합이라고 볼 수 있다. 그럼 이 문제는 어떻게 해결할 수 있을까?
해결방법은 모델을 좀 더 복잡하게 만들면 된다. 즉 훈련 세트에 더 잘 맞도록 모델을 만들면 테스트 세트의 점수는 낮아질 것이다. 우리가 사용한 k-최근접 이웃 알고리즘으로 모델을 복잡하게 만드는 방법은 이웃의 개수 k값을 줄이는 것이다. k-최근접 이웃 알고리즘의 기본값은 5인데, 이를 3으로 낮추어보겠다.
#이웃의 개수를 3으로 설정
knr.n_neighbors = 3
#모델을 다시 훈련시키기
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target))
0.9804899950518966
k값을 줄였더니 훈련세트의 점수가 더 높아졌다. 그럼 이제 테스트 세트의 점수를 확인해보자.
print(knr.score(test_input, test_target))
0.9746459963987609
예상대로 테스트 세트의 점수가 훈련 세트의 점수보다 낮아졌기에 과소적합 문제를 해결한 것 같다. 또한 두 점수의 차이가 크지 않기에 이 모델이 과대적합되었다고도 볼 수 없을 것 같다.
위의 내용을 간단히 요약해보자.
과대적합은 모델의 훈련 세트 성능이 테스트 세트 성능보다 훨씬 높을 때 일어난다. 모델이 훈련 세트에 너무 집착해 데이터에 내제된 거시적인 패턴을 감지하지 못하는 문제를 말한다. 이런 과대적합 문제를 해결하기 위해서는 k-최근접 이웃의 경우 k값을 늘리는 방법을 사용해 모델을 덜 복잡하도록 만들어 줘야한다.
과소적합은 훈련 세트와 테스트 세트 성능이 모두 동일하게 낮거나 테스트 세트 성능이 오히려 더 높을 때 발생한다. 이런 경우 더 복잡한 모델을 사용해 훈련 세트에 잘 맞는 모델을 만들어야 한다. 과소적합 문제를 해결하기 위해서는 k-최근접 이웃의 경우 k값을 줄이는 방법을 사용해 모델을 더 복잡하게 만들어 줘야한다.
선형회귀는 k-최근접 이웃 회귀의 한계를 보완할 수 있다. 그럼 k-최근접 이웃의 한계에는 무엇이 있을까? k-최근접 이웃은 말 그대로 내가 예측하고자 하는 값에서 가장 근접한 값들의 평균을 산출해 예측을 하기 때문에 내가 예측하고자 하는 값의 인접값이 없으면 정확한 예측이 어렵다. 말로 설명하면 복잡하니 직접 눈으로 보자.
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)
#길이가 50cm인 농어의 무게 예측하기
print(knr.predict([[50]]))
[1033.33333333]
이 모델은 50cm크기의 농어 무게를 약 1033g으로 예측했다. 그렇다면 길이가 100cm인 농어의 무게를 예측해보자. 마지막 코드만
print(knr.predict([[100]]))
이렇게 바꿔주면 된다.
[1033.33333333]
이 모델은 50cm크기와 100cm크기 모두 약 1033g으로 예측했다. 상식적으로 농어의 크기가 늘어나면 무게는 당연히 늘어날 것인데, 상식적으로 이해가 가지 않는 부분이다. 그렇다면 이러한 현상은 왜 일어난 것일까?
이는 k-최근접 이웃의 한계 때문이다. 그래프를 그려서 설명해보겠다.
import matplotlib.pyplot as plt
#50cm 농어의 이웃 구하기
distance, indexes = knr.kneighbors([[50]])
plt.scatter(train_input, train_target)
#훈련 세트중 이웃 샘플만 다시 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker = 'D')
#50cm 농어 데이터
plt.scatter(50,1033, marker = '^')
#100cm 농어 데이터
plt.scatter(100, 1033, marker = '^')
plt.xlabel('lenght')
plt.ylabel('weight')
plt.show
인접한 값들을 네모 형태로 표시했고, 예측하고자 하는 값들을 삼각형 표시로 표현해보았다. 앞서 말했듯, k-최근접 이웃 회귀는 예측하고자 하는 값에서 가장 가까운 값(인접값)들의 평균을 산출해 예측하기 때문에 이런 현상이 일어나는 것이다. 그럼 인접값들의 평균을 구해보자.
print(np.mean(train_target[indexes]))
1033.3333333333333
값이 동일하게 나온다. 이렇게 농어의 크기가 늘어나도 무게는 늘어나지 않는 k-최근접 이웃 회귀의 한계를 알아보았다.
선형회귀는 특성과 타깃 사이의 관계를 가장 잘 나타내는 선형 방정식을 찾아 예측값을 예측한다. 선형 회귀가 찾은 특성과 타깃 사이의 관계는 선형 방정식의 계수 또는 가중치에 저장된다. 쉽게 말해 선형 방정식의 기울기와 절편을 얘기하는 것이다. 그렇다면 선형회귀는 어떻게 쓸 수 있을까?
우리가 계속 사용해왔던 사이킷런은 sklearn.linear_model 패키지 아래에 LinearRegression 클래스로 선형 회귀 알고리즘을 사용할 수 있도록 되어있다. 그럼 이를 사용해 훈련해보자.
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
#선형회귀 모델 훈련
lr.fit(train_input, train_target)
print(lr.predict([[50]]))
[1241.83860323]
k-최근접 이웃 회귀를 사용했을 때 보다는 좀 더 현실적인 예측값이 나왔다. 그럼 이 선형 회귀가 학습한 직선을 그려보고 어떻게 이런 값이 나왔는지를 살펴보자.
먼저 직선을 그리기 위해서는 기울기와 절편이 있어야 한다. 그럼 그 값들을 한번 찾아보자.
print(lr.coef_, lr.intercept_)
[39.01714496] -709.0186449535477
여기서 coef와 intercept 를 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터 라고 부른다.
다음은 직선을 그려보자. 직선을 그리기 위해서는 앞서 구한 기울기와 절편을 이용해 15cm와 50cm의 두 점을 이으면 된다.
plt.scatter(train_input, train_target)
#15cm에서 50cm까지 1차 방정식의 그래프 그리기
plt.plot([15,50], [15*lr.coef_+lr.intercept_, 50*lr.coef_+lr.intercept_])
#50cm 농어 데이터
plt.scatter(50,1241.8, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
보이는 이 선이 선형 회귀 알고리즘이 데이터셋에서 찾은 최적의 직선이다. 길이가 50cm인 농어에 대한 예측은 직선의 연장선에 있는것을 볼 수 있다. 하지만 이를 직선으로 표현했기에 동그라미 친 부분에서는 농어의 무게가 음수가 될 수 있는 문제점이 발생한다.
그렇다면 우리는 이를 선형회귀로 해결하는 것이 아닌 다항회귀를 통해 해결해야 함을 알 수 있다. 그렇다면 다항회귀를 위한 2차 방정식의 그래프를 어떻게 구할 수 있을까?
2차 방정식의 그래프를 그리기 위해서는 길이를 제곱한 항이 훈련 세트에 추가되어야 한다. 이는 넘파이를 사용하면 간단하게 만들 수 있다. 전에 배웠던 column_stack() 을 사용하면 된다. 즉 train_input을 제곱한 것과 train_input 두 배열을 나란히 붙이면 된다.
train_poly = np.column_stack((train_input**2, train_input))
test_poly = np.column_stack((test_input**2, test_input))
print(train_poly.shape, test_poly.shape)
(42, 2) (14, 2)
그럼 이제 이 train_poly를 사용해 선형 회귀 모델을 다시 훈련해 보겠다.
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.predict([[50**2, 50]]))
[1573.98423528]
이전 모델보다 더 높은 값을 예측했다. 그럼 이 모델이 학습한 식의 계수와 절편을 한번 출력해보자.
print(lr.coef_, lr.intercept_)
[ 1.01433211 -21.55792498] 116.0502107827827
이 모델은 다음과 같은 그래프를 학습했다.
무게 = 1.01길이^2 - 21.6길이 + 116.05
이렇게 보면 이는 2차 방정식으로 비선형 회귀이지 않나 하는 의심이 들 수 있는데, 길이의 제곱을 다른 변수로 치환을 하게 되면 선형 관계로 표현할 수 있기에 이를 선형 회귀라고 부른다.
이런 방정식을 다항식이라고 부르고, 다항식을 이용한 선형 회귀를 다항 회귀 라고 한다. 그럼 이 2차방정식의 각각 a,b,c의 값을 얻었기에 이 역시 그래프에 표현해보자.
#구간별 직선을 그리기 위해 15~49까지의 정수 배열 만들기
point = np.arange(15,50)
plt.scatter(train_input, train_target)
#15~49까지의 2차 방정식 그래프 그리기
plt.plot(point, 1.01*point**2 - 21.6*point + 116.05)
#50cm 농어 데이터
plt.scatter(50, 1574, marker = '^')
plt.xlabel('lenght')
plt.ylabel('weight')
plt.show
앞서 보여진 단순 선형 회귀모델보다 훨씬 자료를 잘 반영한 그래프가 그려졌다. 이와 같은 그래프는 무게가 음수로 나오는 현상도 없을뿐더러 훈련세트의 경향을 잘 따르는 것으로 보여진다. 그럼 이 그래프가 실제로 훈련 세트의 경향을 잘 따르는지를 판단해보자.
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
0.9706807451768623
0.9775935108325122
훈련 세트와 테스트 세트에 대한 점수가 크게 높아진 것을 볼 수 있다. 하지만 여전히 테스트 세트의 점수가 좀 더 높은 경향이 있다. 즉 과소적합이 조금 남아있음을 알 수 있다. 앞서 말했듯이 과소적합 문제가 나타날 때에는 모델을 좀 더 복잡하게 만들어줘야한다고 말했다. 그럼 좀 더 복잡하게 만들어보는 과정에 대해서 알아보자.
다중 회귀란, 하나의 특성이 아닌 여러 개의 특성을 사용한 선형 회귀를 말한다.
하나의 특성을 사용할 때에 선형 회귀 모델이 학습하는 것은 직선이지만, 2개의 특성을 사용할 때에는 '평면'을 학습한다. 그림을 보면 이해가 빠를 것이다.
다음과 같은 그림이 다중 회귀이다.
#출처: https://thebook.io/080324/0090/
그럼 본론으로 돌아와서 모델을 좀 더 복잡하게 훈련하는 것에 대해서 알아보자. 복잡하게 훈련해 주기 위해서는 많은 데이터들을 사용하게 해주어야한다. 앞서 살펴본 예제들로는 농어의 길이뿐만 아니라 농어의 두께라던지, 농어의 높이 등의 데이터를 추가적으로 넣어주는 것이 방법이 되겠다. 각각의 데이터들을 따로 넣어주는 방법이 아닌, 전에 사용했던 방법인 데이터들을 곱해주어 새로운 특성으로 만들어 주는 방법을 사용해 보겠다.
이렇게 기존의 특성을 이용해서 새로운 특성을 뽑아내는 작업을 특성 공학(feature engineering)이라고 한다. 그럼 농어 데이터들을 불러오는 작업부터 다시 해보겠다.
이전과 달리 농어의 특성이 길이, 높이, 두께 이렇게 3개로 늘어났기 때문에 데이터를 복사해 붙여넣는 것도 번거로울 것이다. 그럼 인터넷에서 바로 데이터들을 다운로드 받아서 사용하는 것이 간편할 것 같은데, 앞서 사용해왔던 넘파이는 이런 기능을 제공해 주지 않는다. 그렇기에 이번에는 판다스(pandas)를 이용해서 데이터들을 다뤄보겠다.
import pandas as pd
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
print(perch_full)
이 코드를 보면 read_csv() 함수로 판다스 데이터프레임을 만든 후 to_numpy() 메서드를 이용해 넘파이 배열로 바꾼 형태이다. 그럼 이제 타깃 데이터들을 불러와보자. 타깃 데이터는 앞서 불러온 방법과 동일한 방법으로 불러온 후 훈련 세트와 타깃 세트로 나눠보겠다.
import numpy as np
#길이 데이터를 추가하지 않은 이유는 앞서 pandas로 불러온 데이터에 포함되어있기 때문
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_full, perch_weight, random_state = 42)
그럼 이 데이터들을 이용해 새로운 특성을 만들어 보도록 하겠다.
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures()
poly.fit([[2,3]])
print(poly.transform([[2,3]]))
[[1. 2. 3. 4. 6. 9.]]
[2,3] 2개의 특성(원소)을 가진 샘플이 6개의 특성을 가진 샘플 [1,2,3,4,6,9] 로 바뀐 것을 볼 수 있다. 2와 3을 각각 제곱한 4와 9가 생성되었고, 2와 3을 곱한 6이 추가되었다. 그럼 1은 어디서 온 것일까?
1은 선형 방정식의 절편을 항상 같은 값이 1인 특성과 곱해지는 계수라고 볼 수 있다. 즉 절편을 위한 항이라고 이해하면 된다. 이러면 특성이 (길이, 높이, 두께, 1)이 되지만, 사이킷런의 선형 모델은 자동으로 절편을 추가하기에 굳이 1을 특성으로 만들 필요가 없다. 그럼 include_bias = False로 지정해 특성을 다시 변환해 보겠다.
poly = PolynomialFeatures(include_bias = False)
poly.fit([[2,3]])
print(poly.transform([[2,3]]))
[[2. 3. 4. 6. 9.]]
절편을 위한 항이 없어지고, 특성의 제곱과, 각각 곱한 항만 추가되었다. 이제 이 방식으로 train_input에 적용하고 train_input을 변환한 데이터를 train_poly에 저장하고 변환된 특성을 활용해 다중 회귀 모델을 훈련해보겠다.
poly = PolynomialFeatures(include_bias = False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
0.9903183436982125
농어의 길이 뿐만 아니라 높이와 두께 모두를 사용하고 각 특성을 제곱하거나, 곱해서 다항 특성을 추가해주니 점수가 굉장히 높게 나온 것을 볼 수 있다.
print(lr.score(test_poly, test_target))
0.9714559911594111
테스트 세트에 대한 점수는 높아지진 않았지만, 농어의 길이만 사용했을 때 있던 과소적합 문제는 더 이상 나타나지 않았다.
그럼 3제곱, 4제곱 항들을 추가적으로 넣어 특성을 더 늘리면 어떨까? PolynomialFeatures 클래스의 degree 매개변수를 이용해 필요한 고차항의 최대 차수를 지정할 수 있다. 5제곱까지 특성을 만들어 출력해보자.
poly = PolynomialFeatures(degree = 5, include_bias = False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
print(train_poly.shape)
(42, 55)
만들어진 특성의 개수가 55개가 되었다.
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
0.9999999999996433
특성의 개수가 늘어나니 훈련의 결과가 눈에 뛰게 좋아지는 것을 볼 수 있다. 그럼 테스트 세트에 대한 점수도 좋을까?
정답은 아니다. 테스트 세트에 대한 점수는 아주 큰 음수값이 출력된다. 특성의 개수를 늘리면 선형 모델은 아주 강력해진다. 훈련 세트에 대해 거의 완벽하게 학습할 수 있다는 것이다. 하지만 이런 모델은 너무 과대적합되는 문제가 발생하기 때문에 테스트 세트에 대해서는 점수가 형편없을 수 밖에 없다.
이렇듯 머신러닝 모델이 훈련 세트를 너무 과도하게 학습하지 못하도록 하는 것을 규제(regularization)라고 한다. 규제를 하는 이유는, 모델이 훈련 세트에 과대적합되지 않도록 하기 위함이다. 선형 회귀 모델의 경우 특성에 곱해지는 계수(또는 기울기)의 크기를 작게 만드는 일이다.
다음은 릿지 회귀에 대해서 알아보자. 먼저, 릿지와 라쏘는 선형 회귀 모델에 규제를 추가한 모델이다. 릿지는 계수의 제곱값을 기준으로 규제하고, 라쏘는 계수의 절댓값을 기준으로 규제한다. 릿지와 라쏘 모두 sklearn.linear_model 패키지 안에 있다. 그럼 릿지 모델을 훈련해 보자. 훈련에 앞서 규제를 위한 특성의 스케일의 정규화를 진행해 보겠다.
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)
from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
0.9896101671037343
#테스트 세트에 대한 훈련점수
print(ridge.score(test_scaled, test_target))
0.9790693977615387
앞서 테스트 점수는 엄청 큰 음수가 나왔었는데 다시 정상으로 돌아온 것을 볼 수 있다. 많은 특성을 사용했지만, 훈련 세트에 너무 과대적합 되지 않아 테스트 세트에서도 좋은 성능을 내고 있다.
릿지와 리쏘 모델을 사용할 때 규제의 양을 임의로 조정할 수 있다. 모델 객체를 만들 때 alpha 매개변수로 규제의 강도를 조절하는데, alpha값이 크면 규제 강도가 세지므로 계수 값을 줄이고 과소적합 되도록 유도한다. 반대로 alpha값이 작으면 계수를 줄이는 역할이 줄어들고, 선형 회귀 모델과 유사해지므로 과대적합 될 가능성이 크다. 그렇다면 적절한 alpha값은 어떻게 찾을까?
alpha값이 크다?
규제강도 세지기 때문에 계수값을 줄이고 과소적합되도록 유도한다.
alpha값이 작다?
계수를 줄이는 역할이 줄어들고 선형 모델에 가까워지기에 과대적합될 가능성이 크다.
적절한 alpha값을 찾기 위해서는 alpha값에 대한 R^2값의 그래프를 그려서 두 그래프가 가깝고 테스트 세트의 점수가 가장 높은 alpha값을 찾으면 된다. 그럼 찾은 alpha값을 가지고 최종 모델을 훈련해 보자.
ridge = Ridge(alpha= 0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))
0.9903815817570367
0.9827976465386928
이 모델은 훈련 세트와 테스트 세트의 점수가 비슷하게 모두 높고 과대적합, 과소적합 사이에서 균형을 맞추고 있는 것을 볼 수 있다.
다음은 라쏘 모델이다.
from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
라쏘도 과대적합을 잘 억제한 것으로 보여진다. 테스트 세트의 점수는 어떨까?
print(lasso.score(test_scaled, test_target))
0.9800593698421883
테스트 세트의 점수도 릿지만큼 좋다. 그럼 앞에서와 같이 alpha값을 바꿔가며 훈련 세트와 테스트 세트에 대한 점수를 계산해보자.
train_score=[]
test_score=[]
alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
#라쏘 모델 만들기
lasso = Lasso(alpha = alpha, max_iter=10000)
#라쏘 모델 훈련하기
lasso.fit(train_scaled, train_target)
#훈련 세트와 테스트 세트 점수 지정하기
train_score.append(lasso.score(train_scaled, train_target))
test_score.append(lasso.score(test_scaled, test_target))
앞에서 했던 것과 동일하게 적절한 alpha값을 찾은 후 그 값으로 다시 모델을 훈련해 보겠다.
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show
적절한 알파값은 10인 것을 알 수 있다.
lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))
0.9888067471131867
0.9824470598706695
이 모델도 잘 훈련된 것을 볼 수 있다. 이로서 이제 규제를 적용한 선형 모델을 사용해 잘 예측할 수 있게 되었다. 오늘은 여기서 마치겠다.