지금까지 공부한 회귀는 y = w0 + w1*x1 + w2*x2 + ... + wn*xn과 같이 독립변수(feature)와 종속변수(target)의 관계가 일차 방정식 형태로 표현된 회귀였다. 하지만 세상의 모든 관계를 직선으로만 표현할 수 없다.

즉, 다항 회귀는 y = w0 + w1*x1 + w2*x2 + w3*x1*x2 + w4*x1^2 + w5*x2^2과 같이 회귀식이 독립변수의 단항식이 아닌 2차, 3차 방정식과 같은 다항식으로 표현되는 것을 지칭한다.

 

데이터 세트에 대해서 피처 X에 대해 타겟 y값의 관계를 단순 선형회귀 직선형으로 표현한 것보다 다항회귀 곡선형으로 표현한 것이 더 예측 성능이 높을 수 있음

 

 

 

 

 

한 가지 주의할 것은 다항회귀는 선형회귀이다. 회귀에서 선형회귀/비선형회귀를 나누는 기준은 회귀계수가 선형/비선형인지에 따른 것이지 독립변수의 선형/비선형 여부와는 무관하다.

 

사이킷런은 다항회귀를 바로 API로 제공하지 않는다. 

→ PolynomialFeatures 클래스로 원본 단항 피처들을 다항 피처들로 변환한 데이터 세트에 LinearRegression 객체를 적용하여 다항회귀 구현(아래의 과정을 각각 수행하는 것보다, pipeline 클래스를 이용해 두 가지 과정을 결합하여 한 번에 다항회귀를 구현하는 것이 일반적이고 코드가 더 명료함)

단항 피처[x1, x2]를 degree=2, 즉 2차 다항 피처로 변환하면

(x1+x2)^2 의 식 전개에 대응되는 [1, x1, x2, x1x2, x1^2, x2^2]의 다항 피처들로 변환

                                                     ↓

1차 단항 피처들의 값이 [x1, x2] = [0, 1] 일 경우

2차 다항 피처들의 값은 [1, x1=0, x2=1, x1x2=0, x1^2=0, x2^2=1]

형태인 [1, 0, 1, 0, 0, 1] 로 변환

 

 

 

마찬가지로, 단항 피처[x1, x2]를 degree=3, 즉 3차 다항 피처로 변환하면

(x1+x2)^3 의 식 전개에 대응되는 [1, x1, x2, x1x2, x1^2, x2^2, x1^3, x1^2x2, x1x2^2, x2^3]의 다항 피처들로 변환

 

참고로, PolynomialFeatures 클래스는 원본 피처 데이터 세트를 기반으로 degree(차수)에 따른 다항식을 적용하여 새로운 피처들을 생성하는 클래스로 피처 엔지니어링의 기법중의 하나임


PolynomialFeatures를 이용해 다항식 변환을 연습해보자

from sklearn.preprocessing import PolynomialFeatures
import numpy as np

# 단항식 생성, [[0,1],[2,3]]의 2X2 행렬 생성
X = np.arange(4).reshape(2,2)
print('일차 단항식 계수 feature:\n', X)

# degree=2인 2차 다항식으로 변환
poly = PolynomialFeatures(degree=2)
poly.fit(X)
poly_ftr = poly.transform(X)
print('변환된 2차 다항식 계수 feature:\n', poly_ftr)

이번에는 3차 다항회귀 함수를 임의로 설정하고 이의 회귀계수를 예측해보자.

3차 다항식 결정값을 구하는 함수 polynomial_func(X) 생성. y = 1+ 2x_1 + 3x_1^2 + 4x_2^3

def polynomial_func(X):
    y = 1 + 2*X[:,0] + 3*X[:,0]**2 + 4*X[:,1]**3
    return y

X = np.arange(4).reshape(2,2)

print('일차 단항식 계수 feature:\n', X)
y = polynomial_func(X)
print('삼차 다항식 결정값:\n', y)

from sklearn.linear_model import LinearRegression

# 3차 다항식 변환
poly_ftr = PolynomialFeatures(degree=3).fit_transform(X)
print('3차 다항식 계수 feature:\n', poly_ftr)

# LinearRegression에 3차 다항식 계수 feature와 3차 다항식 결정값으로 학습 후 회귀계수 확인
model = LinearRegression()
model.fit(poly_ftr, y)
print('Polynomial 회귀계수\n', np.round(model.coef_, 2))

일차 단항식 계수 피처는 2개였지만, 3차 다항식 변환 후에는 다항식 계수 피처가 10개로 늘어난다.이 피처 데이터 세트에 LinearRegression을 통해 3차 다항회귀 형태의 다항회귀를 적용하면 회귀계수가 10개로 늘어난다. 

 

pipeline을 이용해 한 번에 다항회귀를 구현해보자.

from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
import numpy as np

def polynomial_func(X):
    y = 1 + 2*X[:,0] + 3*X[:,0]**2 + 4*X[:,1]**3
    return y

# Pipeline 객체로 streamline하게 PolynomialFeature 변환과 LinearRegression 연결
model = Pipeline([('poly', PolynomialFeatures(degree=3)),
                 ('linear', LinearRegression())])
X = np.arange(4).reshape(2,2)
y = polynomial_func(X)

model = model.fit(X, y)
print('Polynomial 회귀계수\n', np.round(model.named_steps['linear'].coef_, 2))


다항회귀를 이용해 보스턴 주택가격을 예측해보자

import numpy as np
import pandas as pd

from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error, r2_score

# boston 데이터셋 로드
boston = load_boston()

# boston 데이타셋 DataFrame 변환 
bostonDF = pd.DataFrame(boston.data, columns=boston.feature_names)

# boston dataset의 target array는 주택 가격임. 이를 PRICE 컬럼으로 DataFrame에 추가함. 
bostonDF['PRICE'] = boston.target
print('보스턴 데이터셋 shape:', bostonDF.shape)

y_target = bostonDF['PRICE']
X_features = bostonDF.drop(['PRICE'], axis=1, inplace=False)

X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.3, random_state=156)

# Pipeline 이용: PolynomialFeatures 변환, LinearRegression 적용을 순차적으로 결합
p_model = Pipeline([('poly', PolynomialFeatures(degree=3, include_bias=False)),
                   ('linear', LinearRegression())])

p_model.fit(X_train, y_train)
y_preds = p_model.predict(X_test)
mse = mean_squared_error(y_test, y_preds)
rmse = np.sqrt(mse)


print('MSE:{0:.3f}, RMSE:{1:.3f}'.format(mse, rmse))
print('Variance score:{0:.3f}'.format(r2_score(y_test, y_preds)))


다항회귀를 이용한 underfitting, overfitting 이해

피처 X에 대해 타겟 y값의 관계를 단순 선형회귀 직선형으로 표현한 것보다 다항회귀 곡선형으로 표현한 것이 더 예측 성능이 높을 수 있다. 하지만 다항회귀의 차수(degree)를 높일수록 학습 데이터에만 너무 맞춘 학습이 이뤄져서 정작 테스트 데이터 환경에서는 오히려 예측 정확도가 떨어진다. 즉, 차수가 높아질수록 과적합 문제가 크게 발생한다.

# 데이터 생성
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
%matplotlib inline

# X는 0이상 1미만의 랜덤값을 30개 생성후 순서대로 샘플링
np.random.seed(0)
n_samples = 30
X = np.sort(np.random.rand(n_samples))

# X값에 대해 Cosine 변환값을 반환하는 함수
def true_fun(X):
    return np.cos(1.5 * np.pi * X)

# y는 X의 Cosine 값에서 약간의 노이즈를 더한 값
y = true_fun(X) + np.random.randn(n_samples) * 0.1
plt.scatter(X, y)

plt.figure(figsize=(14, 5))
degrees = [1, 4, 15]

# 다항회귀 차수를 1, 4, 15로 각각 변화시키면서 비교
for i in range(len(degrees)):
    ax = plt.subplot(1, len(degrees), i+1)
    plt.setp(ax, xticks=(), yticks=())
    
    # 차수별로 Polynomial 변환
    pipeline = Pipeline([('poly', PolynomialFeatures(degree=degrees[i], include_bias=False)),
                        ('linear', LinearRegression())])
    pipeline.fit(X.reshape(-1,1), y)
    
    # 교차검증으로 다항회귀 평가
    scores = -cross_val_score(pipeline, X.reshape(-1,1), y, scoring='neg_mean_squared_error', cv=10)
    # 회귀계수 추출
    print('\ndegree {0} 회귀계수:{1}'.format(degrees[i], np.round(pipeline.named_steps['linear'].coef_, 2)))
    print('degree {0} mse:{1}'.format(degrees[i], np.round(np.mean(scores), 2))) # 교차검증별 mse를 평균냄
    
    # 0부터 1까지 테스트 데이터 세트를 100개로 나눠 예측 수행
    # 테스트 데이터 세트에 회귀 예측을 수행하고 예측 곡선과 실제 곡선을 그려서 비교
    X_test = np.linspace(0,1,100)
    # 예측값 곡선
    plt.plot(X_test, pipeline.predict(X_test[:, np.newaxis]), label='Model')
    # 실제값 곡선
    plt.plot(X_test, true_fun(X_test), '--', label='True function')
    # 학습 데이터
    plt.scatter(X, y, edgecolor='b', s=20, label='Samples')
    
    plt.xlabel('x'); plt.ylabel('y'); plt.xlim((0,1)); plt.ylim((-2,2)); plt.legend(loc='best')
    plt.title('degree {0}\nMSE = {1:.2e}(+/- {2:.2e})'.format(degrees[i], scores.mean(), scores.std()))
    
plt.show()

실선은 예측곡선, 점선은 실제 데이터 세트(테스트 데이터) X(X_test), y(true_func(X_test))의 코사인 곡선이다. 학습데이터는 0부터 1까지 30개의 랜덤의 X값과 그에 따른 코사인 y값에 노이즈를 추가해 구성했으며 MSE 평가는 학습 데이터를 10fold로 나누어 측정해서 평균한 것이다.

  • degree1 예측곡선은 단순한 직선으로 단순 선형회귀와 똑같다. 실제 데이터 세트인 코사인 데이터 세트를 직선으로 예측하기에는 너무 단순해 보인다. 예측 곡선이 학습 데이터의 패턴을 제대로 반영하지 못하고 있는 과소적합 모델이다. MSE 값은 약 0.41이다.
  • degree4 예측곡선은 실제 데이터 세트와 유사하다. 노이즈까지 예측하지는 못했지만, 학습 데이터 세트를 비교적 잘 반영해 코사인 곡선 기반으로 테스트 데이터를 잘 예측한 곡선을 가진 모델이다. MSE 값은 약 0.04로 가장 뛰어난 예측 성능을 나타낸다.
  • degree15 예측곡선은 학습 데이터의 노이즈까지 지나치게 반영한 결과, 예측곡선이 학습 데이터 세트만 정확히 예측하고, 테스트 데이터의 실제 곡선과는 완전히 다른 형태이다. 결과적으로 학습 데이터에 너무 충실하게 맞춘 과적합이 심한 모델이고 MSE 값은 182815433이 될 정도로 말도 안되는 오류값이 발생했다.  
  • degree 15 회귀계수는 degree1, 4와 비교할 수 없을 정도로 매우 큰 값이다. degree 15라는 복잡한 다항식을 만족하기 위해 계산된 회귀계수는 결국 현실과 너무 동떨어진 예측 결과를 보여준다.

→결국 좋은 예측모델은 degree1과 같이 학습 데이터의 패턴을 지나치게 단순화한 과소적합 모델도 아니고, degree15와 같이 모든 학습 데이터의 패턴을 하나하나 감안한 지나치게 복잡한 과적합 모델도 아닌, 학습 데이터의 패턴을 잘 반영하면서 복잡하지 않은 균형 잡힌 모델이다.


편향-분산 트레이드오프(Bias-Variance Trade off)

degree1의 모델: 매우 단순화된 모델로 데이터를 충분히 표현하지 못함 → 높은 편향을 가짐(낮은 분산) → 지나치게 특정 방향으로 치우쳤다는 의미 → 과소적합(underfitting)

degree15의 모델: 학습 데이터 하나하나의 특성을 반영하면서 매우 복잡한 모델로 데이터를 너무 과하게 표현함 → 높은 분산을 가짐(낮은 편향) → 지나치게 높은 변동성을 가졌다는 의미 → 과적합(overfitting)

 

편향과 분산은 한쪽이 높으면 한쪽이 낮아지는 트레이드오프 관계에 있다. 따라서 둘을 적절하게 조절하여 전체 오류가 가장 낮아지는 '골디락스' 지점을 찾는 것이 매우 중요하다. 즉, 편향-분산 트레이드오프는 머신러닝이 극복해야 할 가장 중요한 이슈 중 하나로 머신러닝 모델을 만들때 과소적합, 과적합 모델이 아닌 적절한 모델을 만들어야 한다.

 

+ Recent posts