by museonghwang

다항 회귀와 과(대)적합/과소적합 이해

|

다항 회귀 이해

회귀가 독립변수의 단항식이 아닌 2차, 3차 방정식과 같은 다항식으로 표현되는 것다항(Polynomial) 회귀 라고 합니다. 즉, 다항 회귀는 다음과 같이 표현할 수 있습니다.


\[y = w_0 + w_1 * x_1 + w_2 * x_2 + w_3 * x_1 * x_2 + w_4 * x_1^2 + w_5 * x_2^2\]


한 가지 주의할 것은 다항 회귀는 선형 회귀 입니다. 회귀에서 선형 회귀/비선형 회귀를 나누는 기준은 회귀 계수가 선형/비선형 인지에 따른 것 이지, 독립변수의 선형/비선형 여부와는 무관합니다. 새로운 변수인 Z를 $z = [x_1, x_2, x_1*x_2, x_1^2, x_2^2]$ 라고 한다면 다음과 같습니다.


\[y = w_0 + w_1 * z_1 + w_2 * z_2 + w_3 * z_3 + w_4 * z_4 + w_5 * z_5\]


위와 같이 표현할 수 있기에 다항 회귀는 선형 회귀 입니다. 다음 그림을 보면 데이터 세트에 대해서 피처 X에 대해 Target Y 값의 관계를 단순 선형 회귀 직선형으로 표현한 것보다 다항 회귀 곡선형으로 표현한 것이 더 예측 성능이 높습니다.

image


사이킷런은 다항 회귀를 위한 클래스를 명시적으로 제공하지 않습니다. 대신 다항 회귀 역시 선형 회귀이기 때문에 비선형 함수를 선형 모델에 적용시키는 방법을 사용 해 구현합니다.

이를 위해 사이킷런은 PolynomialFeatures 클래스를 통해 피처를 Polynomial(다항식) 피처로 변환 합니다. PolynomialFeatures 클래스는 degree 파라미터를 통해 입력받은 단항식 피처를 degree 에 해당하는 다항식 피처로 변환합니다. 다른 전처리 변환 클래스와 마찬가지로 PolynomialFeatures 클래스는 fit(), transform() 메서드를 통해 이 같은 변환 작업을 수행합니다.

다음 예제는 PolynomialFeatures 를 이용해 단항값 $[x_1, x_2]$ 2차 다항값으로 $[1, x_1, x_2, x_1^2, x_1x_2, x_2^2]$ 로 변환하는 예제입니다.

from sklearn.linear_model import LinearRegression
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차 다항식으로 변환하기 위해 PolynomialFeatures를 이용하여 변환
poly = PolynomialFeatures(degree=2)
poly.fit(X)
poly_ftr = poly.transform(X)
print('변환된 2차 다항식 계수 feature:\n', poly_ftr)
[output]
일차 단항식 계수 feature:
 [[0 1]
 [2 3]]
변환된 2차 다항식 계수 feature:
 [[1. 0. 1. 0. 0. 1.]
 [1. 2. 3. 4. 6. 9.]]


단항 계수 피처 $[x_1, x_2]$ 를 2차 다항 계수 $[1, x_1, x_2, x_1^2, x_1x_2, x_2^2]$ 로 변경하므로 첫 번째 입력 단항 계수 피처 $[x_1=0, x_2=1]$ 은 $[1, x_1=0, x_2=1, x_1^2=0, x_1x_2=0, x_2^2=1]$ 형태인 $[1, 0, 1, 0, 0, 1]$ 로 변환되고, 두 번째 입력 단항 계수 피처 $[x_1=2, x_2=3]$ 은 $[1, 2, 3, 4, 6, 9]$ 로 변환됩니다.


이렇게 변환된 Polynomial 피처에 선형 회귀를 적용해 다항 회귀를 구현 합니다. 이번에는 3차 다항 계수를 이용해 3차 다항 회귀 함수식을 PolynomialFeaturesLinearRegression 클래스를 이용해 유도해 보겠습니다.

이를 위해 3차 다항 회귀 함수를 임의로 설정하고 회귀 계수를 예측할 것 입니다. 먼저 3차 다항회귀의 결정 함수식은 다음과 같이 $y = 1 + 2x_1 + 3x_1^2 + 4x_2^3$ 로 설정하고 이를 위한 함수 polynomial_func() 를 만듭니다. 해당 함수는 3차 다항 계수 피처 값이 입력되면 결정 값을 반환합니다.

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

X = np.arange(0,4).reshape(2,2)
print('일차 단항식 계수 feature: \n', X)
y = polynomial_func(X)
print('삼차 다항식 결정값: \n', y)
[output]
일차 단항식 계수 feature: 
 [[0 1]
 [2 3]]
삼차 다항식 결정값: 
 [  5 125]


이제 일차 단항식 계수를 삼차 다항식 계수로 변환하고, 이를 선형 회귀에 적용하면 다항 회귀로 구현됩니다. PolynomialFeatures(degree=3) 은 단항 계수 피처 $[x_1, x_2]$ 를 3차 다항 계수 $[1, x_1, x_2, x_1^2, x_1x_2, x_2^2, x_1^3, x_1^2x_2, x_1x_2^2, x_2^3]$ 과 같이 10개의 다항 계수로 변환합니다.

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

# Linear Regression에 3차 다항식 계수 feature와 3차 다항식 결정값으로 학습 후 회귀 계수 확인
model = LinearRegression()
model.fit(poly_ftr, y)
print('Polynomial 회귀 계수\n', np.round(model.coef_, 2))
print('Polynomial 회귀 Shape :', model.coef_.shape)
[output]
3차 다항식 계수 feature: 
 [[ 1.  0.  1.  0.  0.  1.  0.  0.  0.  1.]
 [ 1.  2.  3.  4.  6.  9.  8. 12. 18. 27.]]
Polynomial 회귀 계수
 [0.   0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]
Polynomial 회귀 Shape : (10,)


일차 단항식 계수 피처는 2개였지만, 3차 다항식 Polynomial 변환 이후에는 다항식 계수 피처가 10개로 늘어납니다. 이 피처 데이터 세트에 LinearRegression 을 통해 3차 다항 회귀 형태의 다항 회귀를 적용하면 회귀 계수가 10개로 늘어납니다. 10개의 회귀 계수 [0, 0.18, 0.18, 0.36, 0.54, 0.72, 0.72, 1.08, 1.62, 2.34] 가 도출됐으며 원래 다항식 $y = 1 + 2x_1 + 3x_1^2 + 4x_2^3$ 의 계수 값인 [1, 2, 0, 3, 0, 0, 0, 0, 0, 4] 와는 차이가 있지만 다항 회귀로 근사하고 있음 을 알 수 있습니다. 이처럼 사이킷런은 PolynomialFeatures 로 피처를 변환한 후에 LinearRegression 클래스로 다항 회귀를 구현합니다.

바로 이전 예제와 같이 피처 변환과 선형 회귀 적용을 각각 별도로 하는 것보다는 사이킷런의 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 하게 Polynomial Feature변환과 Linear Regression을 연결
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))
[output]
Polynomial 회귀 계수
 [0.   0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]


다항 회귀를 이용한 과소적합 및 과적합 이해

다항 회귀는 피처의 직선적 관계가 아닌 복잡한 다항 관계를 모델링할 수 있으며, 다항식의 차수가 높아질수록 매우 복잡한 피처 간의 관계까지 모델링이 가능합니다. 하지만 다항 회귀의 차수(degree)를 높일수록 학습 데이터에만 너무 맞춘 학습이 이뤄져서 정작 테스트 데이터 환경에서는 오히려 예측 정확도가 떨어집니다. 즉, 차수가 높아질수록 과적합의 문제가 크게 발생합니다.

다항 회귀를 이용해 과소적합과 과적합의 문제를 잘 보여주는 예제를 살펴보겠습니다. 피처 X와 target y가 잡음(Noise)이 포함된 코사인(Cosine) 그래프 관계 를 가지도록 만들고, 이에 기반해 다항 회귀의 차수를 변화시키면서 그에 따른 회귀 예측 곡선과 예측 정확도를 비교하는 예제 입니다.

import numpy as np
np.random.seed(0)
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
%matplotlib inline

# 임의의 값으로 구성된 X값에 대해 코사인 변환 값을 반환.
def true_fun(X):
    return np.cos(1.5 * np.pi * X)

# X는 0부터 1까지 30개의 임의의 값을 순서대로 샘플링한 데이터입니다.
n_samples = 30
X = np.sort(np.random.rand(n_samples))

# y 값은 코사인 기반의 true_fun()에서 약간의 노이즈 변동 값을 더한 값입니다.
y = true_fun(X) + np.random.randn(n_samples) * 0.1
plt.scatter(X, y)

image


이제 예측 결과를 비교할 다항식 차수를 각각 1, 4, 15로 변경하면서 예측 결과를 비교하겠습니다. 다항식 차수별로 학습을 수행한 뒤 cross_val_score() 로 MSE 값을 구해 차수별 예측 성능을 평가하고, 0부터 1까지 균일하게 구성된 100개의 테스트용 데이터 세트를 이용해 차수별 회귀 예측 곡선을 그려보겠습니다

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

# 다항 회귀의 차수(degree)를 1, 4, 15로 각각 변화시키면서 비교합니다.
for i in range(len(degrees)):
    ax = plt.subplot(1, len(degrees), i + 1)
    plt.setp(ax, xticks=(), yticks=())
    
    # 개별 degree별로 Polynomial 변환합니다.
    polynomial_features = PolynomialFeatures(degree=degrees[i], include_bias=False)
    linear_regression = LinearRegression()
    pipeline = Pipeline(
        [
            ("polynomial_features", polynomial_features),
            ("linear_regression", linear_regression)
        ]
    )
    pipeline.fit(X.reshape(-1, 1), y)
    
    # 교차 검증으로 다항 회귀를 평가합니다.
    scores = cross_val_score(
        pipeline,
        X.reshape(-1, 1),
        y,
        scoring="neg_mean_squared_error",
        cv=10
    )
    
    # Pipeline을 구성하는 세부 객체를 접근하는 named_steps['객체명']을 이용해 회귀계수 추출
    coefficients = pipeline.named_steps['linear_regression'].coef_
    print('\nDegree {0} 회귀 계수는 {1} 입니다.'.format(degrees[i], np.round(coefficients, 2)))
    print('Degree {0} MSE 는 {1} 입니다.'.format(degrees[i], -1*np.mean(scores)))
          
    # 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 {}\nMSE = {:.2e}(+/- {:.2e})".format(degrees[i], -scores.mean(), scores.std()))
    
plt.show()
[output]
Degree 1 회귀 계수는 [-1.61] 입니다.
Degree 1 MSE 는 0.4077289625098685 입니다.

Degree 4 회귀 계수는 [  0.47 -17.79  23.59  -7.26] 입니다.
Degree 4 MSE 는 0.04320874987232064 입니다.

Degree 15 회귀 계수는 [-2.98293000e+03  1.03899390e+05 -1.87416123e+06  2.03716219e+07
 -1.44873283e+08  7.09315363e+08 -2.47065792e+09  6.24561050e+09
 -1.15676510e+10  1.56894936e+10 -1.54006023e+10  1.06457264e+10
 -4.91377530e+09  1.35919645e+09 -1.70380786e+08] 입니다.
Degree 15 MSE 는 181238256.56423894 입니다.

image


  • Degree 1 예측 곡선(MSE : 약 0.41)
    • 과소적합 : 예측 곡선이 학습 데이터의 패턴을 제대로 반영하지 못하고 있음.
    • 고편향(High Bias)성을 가짐 : 매우 단순화된 모델로서 지나치게 한 방향성으로 치우친 경향
  • Degree 4 예측 곡선(MSE : 약 0.04)
    • 적합 : 학습 데이터 세트를 비교적 잘 반영해 코사인 곡선 기반으로 테스트 데이터를 잘 예측.
  • Degree 15 예측 곡선(MSE : 약 182581084.83)
    • 과적합 : 예측 곡선이 데이터 세트의 변동 잡음값까지 지나치게 반영함.
    • 예측 곡선이 학습 데이터 세트만 정확히 예측하고, 테스트 값의 실제 곡선과는 완전히 다른 형태의 예측 곡선이 만들어짐.
    • 복잡한 다항식을 만족하기 위해 계산된 회귀 계수는 현실과 너무 동떨어진 예측 결과를 보여줌.
    • 고분산(High Variance)성을 가짐 : 학습 데이터 하나하나의 특성을 반영하면서 매우 복잡한 모델이 되어, 지나치게 높은 변동성을 가짐.


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


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

편향-분산 트레이드오프(Bias-Variance Trade off) 는 머신러닝이 극복해야 할 가장 중요한 이슈 중 하나입니다.

image


위 그림은 편향과 분산의 고/저의 의미를 직관적으로 잘 표현하고 있습니다.

  • 상단 왼쪽의 저편향/저분산(Low Bias/Low Variance)
    • 예측 결과가 실제 결과에 매우 잘 근접, 예측 변동이 크지 않고 특정 부분에 집중돼 있는 아주 뛰어난 성능을 보여줌.
  • 상단 오른쪽의 저편향/고분산(Low Bias/High Variance)
    • 예측 결과가 결과에 비교적 근접, 예측 결과가 실제 결과를 중심으로 꽤 넓은 부분에 분포돼 있음.
  • 하단 왼쪽의 고편향/저분산(High Bias/Low Variance)
    • 정확한 결과에서 벗어나면서, 예측이 특정 부분에 집중돼 있음.
  • 하단 오른쪽의 고편향/고분산(High Bias/High Variance)
    • 정확한 예측 결과를 벗어나면서, 넓은 부분에 분포돼 있음.


일반적으로 편향과 분산은 한쪽이 높으면 한쪽이 낮아지는 경향(Bias-Variance Trade off) 이 있습니다.

  • 과소적합
    • 편향이 높으면 분산은 낮아지는 경향
    • 즉, 높은 편향/낮은 분산에서 과소적합되기 쉬움.
  • 과적합
    • 분산이 높으면 편향이 낮아지는 경향
    • 즉, 낮은 편향/높은 분산에서 과적합되기 쉬움.


image


위 그림은 편향과 분산의 관계에 따른 전체 오류 값(Total Error) 의 변화를 잘 보여줍니다. 편향이 너무 높으면 전체 오류가 높습니다. 편향을 점점 낮추면 동시에 분산이 높아지고 전체 오류도 낮아지게 됩니다. 편향을 낮추고 분산을 높이면서 전체 오류가 가장 낮아지는 ‘골디락스’ 지점을 통과하면서 분산을 지속적으로 높이면 전체 오류 값이 오히려 증가하면서 예측 성능이 다시 저하됩니다.

편향과 분산이 서로 트레이드오프를 이루면서, 오류 Cost 값이 최대로 낮아지는 모델을 구축하는 것 이 가장 효율적인 머신러닝 예측 모델을 만드는 방법 입니다.