이전 포스트에서 다룬 모델은 과소적합이 있었다. 훈련 셋의 R^2값이 낮았던 것이다.
이를 해결하기 위해 feature을 더 추가한 다중 회귀 모델을 사용해보자.
다중 회귀란 두 개 이상의 특성을 사용한 선형 회귀를 의미한다.
특성이 두 개라면 3차원의 평면을 학습한다. 만약 특성이 3개 이상이라면 4차원 이상이 되므로 표현할 수 없지만 특성이 3개 이상인 고차원에서의 선형 회귀는 매우 복잡한 모델을 표현할 수 있다.
지금부터 학습할 모델은 농어의 길이뿐만 아니라 농어의 높이와 두께도 함께 사용한다.
또한 앞선 모델과 같이 3개의 특성을 제곱하여 추가한다.
거기다 각 특성을 서로 곱해서 또 다른 특성을 만들 것이다.
즉 '농어 길이 x 농어 높이' 를 새로운 특성으로 만드는 것이다.
이렇게 기존의 특성을 사용해 새로운 특서을 뽑아내는 것을 특성 공학(feature engineering)이라고 부른다.
feature가 세 개가 된 이상 일일이 복사하는 것보다 csv 파일을 바로 불러와 다차원 배열에서 다룰 수 있는 pandas의 데이터프레임(dataframe)을 사용한다.
이를 다시 넘파이 배열로 변환해 학습할 것이다.
csv 파일은 comma seperated values의 약자로 반점으로 구분되어 있는 텍스트 파일이다.
import pandas as pd df = pd.read.csv('https://bit.ly/perch_csv_data') perch_full = df.to_numpy() print(perch_full) # n x 3(feature) 배열특성
import numpy as np 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)
사이킷런은 특성을 만들거나 전처리하기 위한 다양한 클래스를 제공한다. 이러한 클래스를 변환기(transformer)라고 부른다.
이 변환기 클래스엔 일관적인 fit(), transform() 메서드를 제공한다.
앞서 배운 LienarRegression 같은 사이킷런의 모델 클래스는
추정기(estimator)라고 부른다.
여기서 사용할 클래스는 PolynomialFeatures 클래스이다.
이 클래스는 sklearn.preprocessing 패키지에 포함되어 있다.
from sklearn.preprocessing import PolynomialFeatures
2개의 특성 2와 3으로 이루어진 샘플 하나를 적용해보자.
poly = PolynomialFeatures() poly.fit([[2, 3]]) poly.transform([[2, 3]]) # [[1. 2. 3. 4. 6. 9]]위의 코드에서 fit()은 새롭게 만들 특성 조합을 찾고 transform() 매서드는 실제로 데이터를 반환한다.
2개의 특성을 넣어 6개로 바뀌었다. 추가된 4개는 각 특성의 제곱한 항과 특성끼리 곱한 항이다.
첫 번째 1은 뭘까? 선형 방정식의 절편을 항상 값이 1인 특성과 곱해지는 계수라고 볼 수 있다.
이렇게 놓고 보면 특성은 (길이, 높이, 두께, 1)이 된다. 하지만 사이킷런의 선형 모델은 자동으로 절편을 추가하므로 굳이 이렇게 특성을 만들 필요가 없다.(include_bias=False 로 지정하고 다시 해보자.)
poly = PolynomialFeatures(include_bias=False) poly.fit([[2, 3]]) poly.transform([[2, 3]]) # [[2. 3. 4. 6. 9.]]절편을 위한 항이 제거 됐다.
poly = PolynomialFeatures(include_bias=False) poly.fit(train_input) train_poly = poly.transform(train_input) train_poly.shape # (42, 9)특성이 9개로 늘어났다.
poly.get_feature_names_out()['x0', 'x1', 'x2', 'x0^2', 'x0 x1', 'x0 x2', 'x1^2', 'x1 x2', 'x2^2']
test_poly = poly.transform(test_input)
다중 회귀 모델을 훈련하는 것은 선형 회귀 모델을 훈련하는 것이랑 같다.
여러 개의 특성을 사용하여 선형 회귀를 수행하는 것뿐이다.
from sklearn.Linear_model import LinearRegression lr = LinearRegression() lr.fit(train_poly, train_target) lr.score(train_poly, train_target) # 0.9903183436982124결과가 아주 높게 나왔다.
선형 회귀는 특성이 많이질수록 성능이 좋아진다.
lr.score(test_poly, test_target) # 0.9714559911594134테스트에 대한 점수는 그리 높지 않지만 농어의 길이만 사용했을 때 있던 과소적합 문제는 더 이상 나타나지 않았다.
만약 특성을 더 추가하면 어떨까? 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) train_poly.shape # (42, 55)5제곱까지 했을 때 특성의 개수가 55개이다.
이 훈련 셋으로 다시 훈련을 해보자.
lr.fit(train_poly, train_target) lr.score(train_poly, train_target) # 0.99999999991098거의 완벽한 점수이다.
lr.score(test_poly, test_target) # -144.......음수가 나온다. 이게 무슨 일일까?
특성 개수를 크게 늘리면 선형 모델은 아주 강력해진다. 훈련 셋에 대해 거의 완벽하게 학습을 할 수 있다.
하지만 이런 모델은 훈련 셋에 너무 과대적합되므로 테스트 셋엔 형편없는 점수를 만든다.
여기서 사용한 훈련 셋은 42개 밖에 되지 않는다. 42개의 샘플을 55개의 특성으로 훈련하면 거의 완벽해진다.
예를 들어, 42개의 참새를 맞추기 위해 딱 한 번 새총을 쏴야 한다면 참새 떼 중앙을 겨냥하여 가능한 하나의 맞출 가능성을 높여야 한다. 하지만 55번이나 쏠 수 있다면 한 번에 하나씩 모든 참새를 맞출 수 있다.
이 문제를 해결하려면 다시 특성을 줄여야 한다. 하지만 이런 상황은 과대적합을 줄이는 또 다른 방법을 배워 볼 좋은 기회입니다. 이어서 바로 알아보겠다.
규제(regularization)는 머신러닝 모델이 훈련 셋을 너무 과도하게 학습하지 못하도록 훼방을 놓는 것을 말한다.
즉 모델이 훈련 셋에 과대적합되지 않도록 만드는 것이다. 선형 회귀 모델의 경우 특성에 곱해지는 계수(또는 기울기)의 크기를 작게 만드는 것이다.
앞서 55개의 특성으로 훈련한 선형 회귀 모델의 계수를 규제하여 훈련 셋의 점수를 낮추고 대신 테스트 셋의 점수를 높여보겠다.
그 전에 특성의 스케일에 대해 잠시 생각해보자. 2장에서 보았듯이 특성의 스케이리 정규화되지 않으면 여기에 곱해지는 계수 값도 차이가 난다. 일반적으로 선형 회귀 모델에 규제를 적용할 때 계수 값의 크기가 서로 많이 다르면 공정하게 제어되지 않을 수도 있다. 그렇다면 규제를 적용하기 전에 정규화를 먼저해야된다.
이를 위해 사이킷런의StandardScaler클래스를 사용하겠다. 이 클래도 변환기의 하나이다.
from sklearn.preprocessing import StandardScaler ss = StandardScaler() ss.fit(train_poly) train_scaled = ss.transform(train_poly) test_scaled = ss.transform(test_poly)StandardScaler의 클래스에서 훈련 셋에서 학습한 평균과 표준편차는 클래스의 mean, scale 속성에 저장된다.
특성마다 계산하므로 55개의 평군과 표준편차가 들어 있다.
선형 회귀 모델에 규제를 추가한 모델을 릿지(ridge)와 라쏘(lasso)라고 부른다.
두 모델은 규제를 가하는 방법이 다르다.
릿지는 계수를 제곱한 값을 기준으로 규제를 적용하고 라쏘는 계수의 절댓값을 기준으로 규제를 적용한다. (책의 저자는 일반적으로 릿지를 더 선호한다. 라쏘는 계수를 0으로 만들 수 있기 때문이다.)
릿지와 라쏘 모두 sklearn.linear_model 패키지 않에 있다.
from sklearn.linear_model import Ridge ridge = Ridge() ridge.fit(train_scaled, train_target) ridge.score(train_scaled, train_target) # 0.9896101671037343선형 회귀에서는 거의 완벽에 가까웠던 점수가 조금 낮아졌다.
이제 테스트 셋에 대한 정확도를 확인하자.
ridge.score(test_scaled, test_target) # 0.9790693977615398테스트 셋 점수가 정상적으로 돌아왔다. 확실히 많은 특성을 사용했음에도 불구하고 훈련 셋에 너무 과대적합되지 않아 테스트 셋에서도 좋은 성능을 내고 있다.
릿지와 라쏘 모델을 사용할 때 규제의 양을 임의로 조절할 수 있다. 모델 객체를 만들 때 alpha 매개변수로 규제의 강도를 조절한다.
alpha값이 크면 규제 강도가 세지므로 계수 값을 더 줄이고 조금 더 과소적합되도록 유도한다.
alpha값이 작으면 계수를 줄이는 역할이 줄어들고 선형 회귀 모델과 유사해지므로 과대적합될 가능성이 크다.
alpha 값은 릿지 모델이 학습하는 것이 아니라 개발자가 지정해야 하는 값이다. 이렇게 머신러닝 모델이 학습할 수 없고 직접 정해야 하는 파라미터를
하이퍼파라미터(hyperparameter)라고 부른다.
사이킷런과 같은 머신러닝 라이브러리에서 하이퍼파라미터는 클래스와 매서드의 매개변수로 표현된다.
적잘한 alpha 값을 찾는 한 가지 방법은 alpha 값에 대한 R^2 값의 그래프를 그려보는 것이다.
훈련 셋과 테스트 셋의 점수가 가장 낮은 가까운 지점이 최적의 alpha 값이 된다.
이것을 확인하기 위해 alpha 값을 바꿀 때마다 socre() 매서드의 결과를 봐야한다.
train_score = [] test_score = []score() 값을 저장할 리스트
alpha값을 0.001에서 100까지 10배씩 늘려가며 릿지 모델 회귀 모델을 훈련한 다음 훈련 셋과 테스트 셋의 점수를 파이썬 리스트에 저장한다.
alpha_list = [0.001, 0.01, 0.1, 1, 10, 100] for alpha in alpha_list ridge = Ridge(alpha=alpha) ridge.fit(train_scaled, train_target) train_score.append(ridge.score(train_scaled, train_target)) test_score.append(ridge.score(test_scaled, test_target))
그래프를 그리기 위해 alpha값의 scale을 변경해야 한다. 0.001부터 10배씩 늘렸기 때문에 그래프 왼쪽이 너무 촘촘해 진다.
alpha_list에 있는 6개의 값을 동일한 간격으로 나타내기 위해 로그 함수로 바꾸어 지수로 표현한다.
ex) log 0.001 = -3
넘파이 로그 함수는 np.log()와 np.log10()이 있다. 전자는 밑이 e
import matplotlib.pyplot as plt 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()
그래프의 왼쪽은 전형적인 과대적합의 모습이고 오른쪽은 모두 낮아지는 과소적합의 모습이다.
적절한 alpha 값은 두 그래프가 가장 가깝고 테스트 셋의 점수가 가장 높은 -1, 즉 10^-1=0.1이다.
alpha값을 0.1로 하여 최종 모델을 훈련하겠다.
ridge = Ridge(alpha=0.1) ridge.fit(train_scaled, train_target) ridge.score(train_scaled, train_target) # 0.9903815817570366 ridge.score(test_scale, test_target) # 0.9827976465386926이 모델은 훈련 셋과 테스트 셋의 점수가 비슷하게 높고 과대적합과 과소적합 사이에서 균형을 맞추고 있다.
라쏘 회귀를 학습시켜보자.
from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
lasso.score(train_scaled, train_target) # 0.9897898972080961
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)) # 그래프 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()
1이 적절한 alpha값이다.
max_iter은 라쏘 모델이 훈련할 때 최적의 계수를 찾기위한 계산의 반복 횟수이다.(라쏘 모델은 반복적인 계산을 수행한다.)
지정된 횟수가 낮을 때 뜨는 경고이다.
lasso = Lasso(alpha=10) lasso.fit(train_scaled, train_target) print(lasso.score(train_scaled, train_target))# 0.9888067471131867 print(lasso.score(test_scaled, test_target))# 0.9824470598706695훈련이 잘 되었다.
print(np.sum(lasso.coef_ == 0))위의 출력값은 40이다. 55개의 특성 중 40개의 특성의 최적의 계수가 0이라는 것이다.
그럼 라쏘 모델은 15개의 샘플만 사용했다. 이런 특징 때문에 라쏘 모델을 유용한 특성을 골라내는 용도로도 사용할 수 있다.