Search
Duplicate

머신 러닝 교과서/ 회귀 분석으로 연속적 타깃 변수 예측

선형 회귀

선형 회귀는 하나 이상의 특성과 연속적인 타깃 변수 사이의 관계를 모델링하는 것이 목적이다.
회귀 분석은 지도 학습의 하위 카테고리
지도 학습의 다른 카테고리인 분류 알고리즘이 범주형 클래스 레이블을 예측하는 것과 달리 회귀는 연속적인 출력 값을 예측한다.

단변량 선형 회귀

단변량 선형 회귀는 하나의 특성(설명 변수(explanatory variable) x)과 연속적인 타깃(응답 변수(response variable) y) 사이의 관계를 모델링한다.
특성이 하나인 선형 모델의 공식은 다음과 같다.
y=w0+w1xy = w_{0} + w_{1} x
여기서 w0w_{0}는 yy축 절편을 나타내고 w1w_{1}은 특성의 가중치이다.
특성과 타깃 사이의 관계를 나타내는 선형 방정식의 가중치를 학습하는 것이 목적이다.
이 방정식으로 훈련 데이터셋이 아닌 새로운 샘플의 타깃 값을 예측할 수 있다.
선형 방정식으로부터 선형 회귀를 아래 그림과 같이 샘플 포인트를 잘 맞추어 통과하는 직선으로 이해할 수 있다.
데이터에 가장 잘 맞는 직선을 회귀 직선(regression line)이라고도 한다.
회귀 직선과 샘플 포인트 사이의 직선 거리를 오프셋(offset) 또는 예측 오차인 잔차(residual)라고 한다.

다변량 선형 회귀

특성이 하나인 특별한 경우를 단변량 선형 회귀라 하는데, 선형 회귀 모델을 여러 개의 특성이 있는 경우로 일반화한 것을 다변량 선형 회귀라고 한다.
y=w0x0+w1x1+...+wmxm=i=0mwixi=wTxy = w_{0} x_{0} + w_{1} x_{1} + ... + w_{m} x_{m} = \sum_{i=0}^{m} w_{i} x_{i} = w^{T} x
여기서 w0w_{0}는 yy축 절편이고 x0=1x_{0} = 1이다.
아래 그림은 두 개의 특성을 가진 다변량 회귀 모델이 학습한 2차원 초평면을 보여준다.
그림을 보면 알 수 있듯이 다변량 회귀 모델의 그래프는 3차원 산점도만 되어도 이해하기 어렵다. 산점도에서 2차원 초평면을 시각화하는 좋은 방법이 없기 때문.

주택 데이터셋 탐색

1978년 보스톤 교외 지역의 주택 정보에 대한 데이터셋을 이용할 것인데 아래 주소에서 내려 받을 수 있다. (원본 url은 삭제되어서, 이 책의 깃허브에서 내려 받을 수 있음)

데이터프레임으로 주택 데이터셋 읽기

주택 데이터셋은 506개의 샘플이 있으며, 특성은 아래와 같다.
CRIM: 도시의 인당 범죄율
ZN: 2만 5,000평방 피트가 넘는 주택 비율
INDUS: 도시에서 소매 업종이 아닌 지역 비율
CHAS: 찰스 강 인접 여부 (강 주변=1, 그 외=0)
NOX: 일산화질소 농도 (10ppm 당)
RM: 주택의 평균 방 개수
AGE: 1940년 이전에 지어진 자가 주택 비율
DIS: 다섯 개의 보스턴 고용 센터까지 가중치가 적용된 거리
RAD: 방사형으로 뻗은 고속도로까지 접근성 지수
TAX: 10만 달러당 재산세율
PTRATIO: 도시의 학생-교사 비율
B: 1000(Bk-0.63), 여기서 Bk는 도시의 아프리카계 미국인 비율
LSTAT: 저소득 계층의 비율
MEDV: 자가 주택의 중간 가격(1,000 달러 단위)
이 장에서는 주택 가격(MEDV)를 타깃 값으로 삼아서 진행하겠다.
아래 코드를 통해 데이터셋을 다운 받고 DataFrame 형태로 변환할 수 있다.
import pandas as pd df = pd.read_csv('https://raw.githubusercontent.com/rasbt/python-machine-learning-book-2nd-edition/master/code/ch10/housing.data.txt', header=None, sep='\s+') df.columns = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV'] df.head()
Python

데이터셋의 중요 특징 시각화

탐색적 데이터 분석(Exploratory Data Analysis, EDA)은 머신 러닝 모델을 훈련하기 전에 첫 번째로 수행할 중요하고 권장되는 단계이다.
이 절의 나머지 부분에서 EDA 그래픽 도구 중에서 간단하지만 유용한 기법들을 사용해 보겠다.
이런 도구는 이상치를 감지하고 데이터 분포를 시각화 하거나 특성 간의 관계를 나타내는데 도움이 된다.
먼저 산점도 행렬(scatterplot matrix)을 그려서 데이터셋에 있는 특성 간의 상관관계를 한 번에 시각화해 보겠다.
Seaborn 라이브러리의 pairplot 함수를 사용하여 산점도 행렬을 그릴 수 있다.
import matplotlib.pyplot as plt import seaborn as sns cols = ['LSTAT', 'INDUS', 'NOX', 'RM', 'MEDV'] sns.pairplot(df[cols], height=2.5) plt.tight_layout() plt.show()
Python
산점도 행렬을 사용하면 데이터가 어떻게 분포되어 있는지, 이상치를 포함하고 있는지 빠르게 확인할 수 있다.
위 이미지를 보면 RM과 MEDV 사이에 선형적인 관계가 있다는 것을 알 수 있다.
또한 MEDV의 히스토그램을 보면 데이터에 일부 이상치가 있지만 정규 분포 형태를 띠고 있다는 것을 확인할 수 있다.

상관관계 행렬을 사용한 분석

상관관계 행렬(correlation matrix)을 만들어 변수 간의 선형 관계를 정량화 하고 요약해 보겠다.
상관관계 행렬은 주성분 분석에서 본 공분산 행렬(covariance matrix)과 밀접하게 관련되어 있다.
직관적으로 생각하면 상관관계 행렬을 스케일 조정된 공분산 행렬로 생각할 수 있다.
사실 특성이 표준화 되어 있으면 상관관계 행렬과 공분산 행렬이 같다.
상관관계 행렬은 피어슨의 상관관계 계수(Pearson product-moment correlation coefficient)를 포함하고 있는 정방 행렬이다. (종종 피어슨의 rr 이라고 한다)
이 계수는 특성 사이의 선형 의존성을 측정하는데, 상관관계 계수의 범위는 11-1 \sim 1 이다.
r=1r = 1이면 두 특성이 완벽한 양의 상관관계를 가지며 r=0r = 0이면 아무 상관관계가 없고, r=1r = -1이면 완벽한 음의 상관관계를 갖는다.
피어슨의 상관관계 계수는 단순히 두 특성 x,yx, y사이의 공분산을 표준 편차의 곱으로 나눈 것이다.
r=i=1n[(x(i)μx)(y(i)μy)]i=1n(x(i)μx)2i=1n(y(i)μy)2=σxyσxσyr = {\sum_{i=1}^{n}[(x^{(i)} - \mu_{x})(y^{(i)} - \mu_{y})] \over \sqrt{\sum_{i=1}^{n}(x^{(i)} - \mu_{x})^{2}} \sqrt{\sum_{i=1}^{n}(y^{(i)} - \mu_{y})^{2}}} = {\sigma_{xy} \over \sigma_{x} \sigma_{y}}
여기서  μ\mu는 해당 특성의 샘플 평균이고 σxy\sigma_{xy} 는 특성 x,yx, y 사이의 공분산이다. σx,σy\sigma_{x}, \sigma_{y}는 특성의 표준 편차이다.
앞서 그렸던 산점도 행렬로 그렸던 다섯 개의 특성에 넘파이의 corrcoef 함수를 사용해서 상관관계 행렬을 구하고 heatmap으로 그려보겠다.
import numpy as np cm = np.corrcoef(df[cols].values.T) sns.set(font_scale=1.5) hm = sns.heatmap(cm, cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size':15}, yticklabels=cols, xticklabels=cols) plt.tight_layout() plt.show()
Python

최소 제곱 선형 회귀 모델 구현

최소 제곱법(Ordinary Least Squares, OLS)은 이따금 선형 최소 제곱법(Linear least squares)라고도 하는데, 샘플 포인트까지 수직거리의 제곱 합(잔차 또는 오차)을 최소화하는 선형 회귀 직선 모델 파라미터를 추정하는 방법이다.

경사 하강법으로 회귀 모델의 파라미터 구하기

2장에서 선형 활성화 함수를 사용한 인공 뉴런인 아달린(Adaline)을 구현했고, 비용함수 J를 정의하고 경사 하강법(GD)과 확률적 경사 하강법(SGD) 같은 최적화 알고리즘을 사용하여 이 함수를 최소화하는 가중치를 학습했다.
아달린의 비용 함수는 제곱 오차합(Sum of Squared Errors, SSE)으로 OLS에서 사용할 비용 함수와 같다.
J(w)=12i=1n(y(i)y^(i))2J(w) = {1 \over 2} \sum_{i=1}^{n} (y^{(i)} - \hat{y}^{(i)})^{2}
여기서 y^\hat{y}는 예측 값으로 y^=wTx\hat{y} = w^{T} x이다. (12{1 \over 2}을 곱한 것은 경사 하강법의 업데이트 공식을 간단하게 유도하기 위함이다)
근본적으로 OLS 회귀는 단위 계단 함수가 없는 아달린으로 해석할 수 있다. 클래스 레이블 -1이나 1 대신 연속적인 타깃 값을 얻는다.
2장의 아달린의 경사 하강법 코드에서 단위 계단 함수를 제거하여 첫 번째 선형 회귀 모델을 구현해 보겠다.
import numpy as np class LinearRegressionGD(object): def __init__(self, eta=0.001, n_iter=20): self.eta = eta self.n_iter = n_iter def fit(self, X, y): self.w_ = np.zeros(1 + X.shape[1]) self.cost_ = [] for i in range(self.n_iter): output = self.net_input(X) errors = (y - output) self.w_[1:] += self.eta * X.T.dot(errors) self.w_[0] += self.eta * errors.sum() cost = (errors**2).sum() / 2.0 self.cost_.append(cost) return self def net_input(self, X): return np.dot(X, self.w_[1:]) + self.w_[0] def predict(self, X): return self.net_input(X)
Python
LinearRegressionGD를 실행해 보기 위해 주택 데이터셋에서 RM(방 개수) 변수를 특성으로 사용하여 MEDV(주택 가격)를 예측하는 모델을 훈련 시켜 보겠다.
X = df[['RM']].values y = df['MEDV'].values sc_x = StandardScaler() sc_y = StandardScaler() X_std = sc_x.fit_transform(X) y_std = sc_y.fit_transform(y[:, np.newaxis]).flatten() lr = LinearRegressionGD() lr.fit(X_std, y_std) plt.plot(range(1, lr.n_iter+1), lr.cost_) plt.ylabel('SSE') plt.xlabel('Epoch') plt.show()
Python
위 훈련 결과를 그래프로 그리면 아래와 같다.
다섯 번째 에포크에서 수렴한다.
이 선형 회귀 모델이 훈련 데이터에 얼마나 잘 맞는지 그려보자. 이를 위해 훈련 샘플의 산점도와 회귀 직선을 그려주는 헬퍼 함수를 만들어 보자.
def lin_regplot(X, y, model): plt.scatter(X, y, c='steelblue', edgecolor='white', s=70) plt.plot(X, model.predict(X), color='black', lw=2) return None lin_regplot(X_std, y_std, lr) plt.ylabel('Price in $1000s [MEDV] (standarized)') plt.xlabel('Average number of rooms [RM] (standardized)') plt.show()
Python
위 이미지를 보면 y=3y = 3인 직선에 여러 데이터 포인트가 늘어서 있는데, 이는 이 지점에서 초과되는 가격을 잘라 냈다는 것을 의미한다.
어떤 애플리케이션에서는 예측된 출력 값을 원본 스케일로 복원해서 제공해야 하는데, 예측한 가격을 원본 단위 가격으로 되돌리려면 StandardScaler의 inverse_transform 메서드를 호출하면 된다.

사이킷런으로 회귀 모델의 가중치 추정

앞서 회귀 분석을 위한 모델을 구현해 봤는데, 실전에서는 더 효율적인 구현이 필요하다. 사이킷런의 많은 회귀 추정기는 LIBLINEAR 라이브러리와 고수준 최적화 알고리즘을 사용한다.
또 특정 애플리케이션에서 이따금 필요한 표준화되지 않은 변수와도 잘 동작하는 최적화 기법을 사용한다.
from sklearn.linear_model import LinearRegression slr = LinearRegression() slr.fit(X, y) lin_regplot(X, y, slr) plt.ylabel('Price in $1000s [MEDV] (standarized)') plt.xlabel('Average number of rooms [RM] (standardized)') plt.show()
Python
사이킷런의 라이브러리를 사용한 결과는 위에서 직접 만든 것과 대체로 동일하다.

RANSAC을 사용하여 안정된 회귀 모델 훈련

선형 회귀 모델은 이상치(outlier)에 크게 영향을 받을 수 있다. 어떤 상황에서는 데이터의 아주 작은 일부분이 추정 모델의 가중치에 큰 영향을 끼친다.
이상치를 감지하는 통계적 테스트가 많지만 책 범위를 넘어선다. 이상치를 제거하려면 항상 해당 분야의 지식 뿐 아니라 데이터 과학자로서 식견도 필요하다.
이상치를 제거하는 방식 대신 RANSAC(RANdom SAmple Consensus) 알고리즘을 사용하는 안정된 회귀 모델에 대해 알아보겠다.
이 알고리즘은 정상치(inlier)라는 일부 데이터로 회귀 모델을 훈련한다.
반복적인 RANSAC 알고리즘은 다음과 같이 정의할 수 있다.
1.
랜덤하게 일부 샘플을 정상치로 선택하여 모델을 훈련한다.
2.
훈련된 모델에서 다른 모든 포인트를 테스트 한다. 사용자가 입력한 허용 오차 안에 속한 포인트를 정상치에 추가한다.
3.
모든 정상치를 사용하여 모델을 다시 훈련한다.
4.
훈련된 모델과 정상치 간의 오차를 추정한다.
5.
성능이 사용자가 지정한 임계 값에 도달하거나 지정된 반복 횟수에 도달하면 알고리즘을 종료한다. 그렇지 않으면 1단계로 돌아간다.
사이킷런의 RANSACRegressor를 사용하여 RANSAC 알고리즘에 선형 모델을 적용해 보겠다.
from sklearn.linear_model import RANSACRegressor ransac = RANSACRegressor(LinearRegression(), max_trials=100, min_samples=50, loss='absolute_loss', residual_threshold=5.0, random_state=0) ransac.fit(X, y) inlier_mask = ransac.inlier_mask_ outlier_mask = np.logical_not(inlier_mask) line_X = np.arange(3, 10, 1) line_y_ransac = ransac.predict(line_X[:, np.newaxis]) plt.scatter(X[inlier_mask], y[inlier_mask], c='steelblue', edgecolor='white', marker='o', label='Inliers') plt.scatter(X[outlier_mask], y[outlier_mask], c='limegreen', edgecolor='white', marker='s', label='Outliers') plt.plot(line_X, line_y_ransac, color='black', lw=2) plt.ylabel('Price in $1000s [MEDV] (standarized)') plt.xlabel('Average number of rooms [RM] (standardized)') plt.legend(loc='upper left') plt.show()
Python
사이킷런에서 정상치 임계 값의 기본값은 MAD 추정이다. MAD는 타깃 값 y의 중앙값 절대 편차(Median Absolute Deviation)을 의미한다.
적절한 정상치 임계 값은 문제에 따라 다른데, 이것이 RANSAC의 단점 중 하나이다.
최근에는 좋은 정상치 임계 값을 자동으로 선택하기 위한 여러 방법이 개발 되었다.
RANSAC 모델을 훈련한 후 학습된 RANSAC-선형 회귀 모델에서 정상치와 이상치를 얻을 수 있는데, 이를 그래프로 그리면 아래와 같다.
RANSAC 을 사용하면 데이터셋에 있는 이상치의 잠재적인 영향을 감소시킨다. 하지만 이 방법이 본 적 없는 데이터에 대한 예측 성능에 긍정적인 영향을 미치는지 알지 못한다.

선형 회귀 모델의 성능 평가

데이터셋에 있는 모든 변수를 사용하여 다변량 회귀 모델을 훈련해 보겠다.
from sklearn.model_selection import train_test_split X = df.iloc[:, :-1].values y = df['MEDV'].values X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0) slr = LinearRegression() slr.fit(X_train, y_train) y_train_pred = slr.predict(X_train) y_test_pred = slr.predict(X_test) plt.scatter(y_train_pred, y_train_pred - y_train, c='steelblue', marker='o', edgecolor='white', label='Training data') plt.scatter(y_test_pred, y_test_pred - y_test, c='limegreen', marker='s', edgecolor='white', label='Test data') plt.xlabel('Predicted values') plt.ylabel('Residuals') plt.hlines(y=0, xmin=-10, xmax=50, color='black', lw=2) plt.xlim([-10, 50]) plt.tight_layout() plt.show()
Python
모델이 여러 개의 특성을 사용하기 때문에 2차원 그래프로 선형 회귀 직선을 그릴 수 없다. 대신 회귀 모델을 조사하기 위해 잔차(실제 값과 예측 값 사이의 차이 또는 수직 거리) 대 예측 값 그래프를 그릴 수 있다.
잔차 그래프(residual plot)는 회귀 모델을 진단할 때 자주 사용하는 그래프 도구이다. 비선형성과 이상치를 감지하고 오차가 랜덤하게 분포되어 있는지 확인하는데 도움이 된다.
위 훈련의 잔차 그래프는 아래 이미지와 같다.
예측이 완벽하다면 잔차는 0이 된다. 현실적으로 그런 일은 일어나지 않고, 좋은 회귀 모델이라면 오차가 랜덤하게 분포되고 잔차는 중앙선 주변으로 랜덤하게 흩어져야 한다.
잔차 그래프에 패턴이 나타나면 특성에서 어떤 정보를 잡아내지 못하고 잔차로 새어 나갔다고 말한다. 앞선 잔차 그래프에서 이런 현상을 조금 볼 수 있다.
잔차 그래프를 사용하여 이상치를 감지할 수 있는데, 중앙선에서 큰 편차를 낸 포인트가 그것이다.
모델 성능을 정량적으로 측정하는 또 다른 방법은 평균 제곱 오차(Mean Squared Error, MSE)이다. 이 값은 선형 회귀 모델을 훈련하기 위해 최소화하는 제곱 오차합(SSE)의 평균이다.
MSE는 그리드 서치와 교차 검증에서 매개변수를 튜닝하거나 여러 다른 회귀 모델을 비교할 때 유용하다.
MSE는 SSE를 샘플 개수로 나누어 계산한다.
MSE=1ni=1n(y(i)y^(i))2MSE = {1 \over n} \sum_{i=1}^{n}(y^{(i)} - \hat{y}^{(i)})^{2}
훈련 세트와 테스트 세트의 예측에 대한 MSE를 계산해 보자.
from sklearn.metrics import mean_squared_error print('훈련 MSE: %.3f, 테스트 MSE: %.3f' % (mean_squared_error(y_train, y_train_pred), mean_squared_error(y_test, y_test_pred))) #훈련 MSE: 19.958, 테스트 MSE: 27.196
Python
훈련 세트의 MSE가 19.96이고 테스트 세트의 MSE는 27.2로 훨씬 큰데, 이는 모델이 훈련 세트에 과대적합되었다는 신호이다.
이따금 결정계수(coefficient of determination, R2R^{2})가 더 유용할 수 있다.
결정 계수는 모델 성능을 잘 해석하기 위해 만든 MSE의 표준화된 버전으로 생각할 수 있다.
다른 말로 하면 R2R^{2}은 타깃의 분산에서 모델이 잡아낸 비율이다. R2R^{2}은 다음과 같이 정의한다.
R2=1SSESSTR^{2} = 1 - {SSE \over SST}
여기서 SSE는 제곱 오차합이고 SST(total sum of squares)는 전체 제곱합이다.
SST=i=1n(y(i)μy)2SST = \sum_{i=1}^{n} (y^{(i)} - \mu_{y})^{2}
다른 말로 하면 SST는 단순히 타깃의 분산이다.
R2R^{2}이 MSE를 단순히 스케일 조정한 것인지 확인해 보자.
R2=1SSESST=11ni=1n(y(i)y^(i))21ni=1n(y(i) μy(i))2=1MSEVar(y)R^{2} = 1 - {SSE \over SST} = 1 - {{1 \over n} \sum_{i=1}^{n} (y^{(i)} - \hat{y}^{(i)})^{2} \over {1 \over n} \sum_{i=1}^{n} (y^{(i)} -  \mu_{y}^{(i)})^{2}} = 1 - {MSE \over Var(y)}
훈련 세트에서 R2R^{2}은 0과 1사이의 값을 갖는다. 테스트 세트에서는 음수가 될 수 있다. R2=1R^{2} = 1이면  MSE=0MSE = 0이고 모델이 데이터를 완벽히 학습한 것이다.
훈련 세트에서 앞서 만든 모델의 R2R^{2}을 평가하면 0.765라서 아주 나쁘지 않지만 테스트 세트의 R2R^{2}은 0.673 밖에 되지 않는다.
R2R^{2}는 아래와 같이 간단히 계산 가능하다.
from sklearn.metrics import r2_score print('훈련 R^2: %.3f, 테스트 R^2: %.3f' % (r2_score(y_train, y_train_pred), r2_score(y_test, y_test_pred))) # 훈련 R^2: 0.765, 테스트 R^2: 0.673
Python

회귀에 규제 적용

3장에서 설명한 것처럼 규제는 부가 정보를 손실에 더해 과대적합 문제를 방짛나느 한 방법이다.
복잡도에 대한 페널티(penalty)를 유도하여 모델 파라미터의 값을 감소시킨다.
가장 널리 사용하는 선형 회귀 규제 방법은 릿지 회귀(Ridge Regression), 라쏘(Least Absolute Shrinkage and Selection Operator, LASSO), 엘라스틱 넷(Elastic Net) 이다.
릿지 회귀는 단순히 최소 제곱 비용함수에 가중치의 제곱합을 추가한 L2 규제 모델이다.
J(w)Ridge=i=1n(y(i)y^(i))2+λw22J(w)_{Ridge} = \sum_{i=1}^{n} (y^{(i)} - \hat{y}^{(i)})^{2} + \lambda \|w\|_{2}^{2}
여기서 규제 항은 다음과 같다.
L2:λw22=λj=1mwj2L2 : \lambda \|w\|_{2}^{2} = \lambda \sum_{j=1}^{m} w_{j}^{2}
하이퍼파라미터 λ\lambda를 증가시키면 규제 강도가 증가되고 모델의 가중치 값이 감소한다. 절편에 해당하는 w0w_{0}는 규제하지 않는다.
다른 접근 방법으로는 희소한 모델을 만들 수 있는 라쏘이다. 규제 강도에 따라 어떤 가중치는 0이 될 수 있다. 따라서 라쏘를 지도 학습의 특성 선택 기법으로 사용할 수 있다.
J(w)LASSO=i=1n(y(i)y^(i))2+λw1J(w)_{LASSO} = \sum_{i=1}^{n} (y^{(i)} - \hat{y}^{(i)})^{2} + \lambda \|w\|_{1}
여기서 규제 항은 다음과 같다.
L1:λw1=λj=1mwjL1 : \lambda \|w\|_{1} = \lambda \sum_{j=1}^{m} |w_{j}|
LASSO는 m>nm > n이라면 최대 nn개의 특성을 선택하는 것이 한계이다.
엘라스틱 넷은 릿지 회귀와 라쏘의 절충안이다. 희소한 모델을 만들기 위한 L1 페널티와 선택 특성 개수 같은 라쏘의 한계를 극복하기 위한 L2 페널티를 갖는다.
J(w)ElasticNet=i=1n(y(i)y^(i))2+λ1j=1mwj2+λ2j=1mwjJ(w)_{ElasticNet} = \sum_{i=1}^{n} (y^{(i)} - \hat{y}^{(i)})^{2} + \lambda_{1} \sum_{j=1}^{m} w_{j}^{2} + \lambda_{2} \sum_{j=1}^{m} |w_{j}|
이런 규제 선형 모델은 모두 사이킷런에 준비되어 있다. 하이퍼파라미터 λ\lambda를 사용해 규제 강도를 지정해야 하는 것만 제외하고 보통 회귀 모델과 사용법이 비슷하다.
보통 λ\lambda는 k-겹 교차검증으로 최적화한다.
릿지 회귀 모델은 다음과 같이 초기화 한다.
from sklearn.linear_model import Ridge ridge = Ridge(alpha=1.0)
Python
규제 강도는 λ\lambda에 해당하는 alpha 매개변수로 제어한다.
비슷하게 linear_model 모듈에서 라쏘 회귀 모델을 초기화할 수 있다.
from sklearn.linear_model import Lasso lasso = Lasso(alpha=1.0)
Python
마지막으로 ElasticNet 구현을 사용하여 L1과 L2 비율을 조절할 수 있다.
from sklearn.linear_model import ElasticNet elanet = ElasticNet(alpha=1.0, l1_ratio=0.5)
Python
여기서 l1_ratio를 1.0으로 설정하면 ElasticNet은 라쏘 회귀와 동일해진다.

선형 회귀 모델을 다항 회귀로 변환

선형 가정이 어긋날 때 대처할 수 있는 한 가지 방법이 다항식 항을 추가한 다항 회귀 모델을 사용하는 것이다.
y=w0+w1x+w2x2+...+wdxdy = w_{0} + w_{1} x + w_{2} x^{2} + ... + w_{d} x^{d}
여기서 dd는 다항식의 차수를 나타낸다.
다항 회귀를 사용하여 비선형 관계를 모델링하지만 선형 회귀 가중치인 w 때문에 여전히 다변량 선형 회귀 모델로 생각할 수 있다.

사이킷런을 사용하여 다항식 항 추가

사이킷런의 PolynominalFeatures 변환기 클래스를 사용하여 특성이 한 개인 간단한 회귀 문제에 이차 항을 추가하는 방법을 알아보겠다
우선 이차 다항식 항을 추가한다.
from sklearn.preprocessing import PolynomialFeatures X = np.array([258.0, 270.0, 294.0, 320.0, 342.0, 368.0, 396.0, 446.0, 480.0, 586.0])[:, np.newaxis] y = np.array([236.4, 234.4, 252.8, 298.6, 314.2, 342.2, 360.8, 368.0, 391.2, 390.8]) lr = LinearRegression() pr = LinearRegression() quadratic = PolynomialFeatures(degree=2) X_quad = quadratic.fit_transform(X)
Python
비교를 위해 평범한 선형 회귀 모델을 훈련한다.
lr.fit(X, y) X_fit = np.arange(250, 600, 10)[:, np.newaxis] y_lin_fit = lr.predict(X_fit)
Python
다항 회귀를 위해 변환된 특성에서 다변량 회귀 모델을 훈련한다.
pr.fit(X_quad, y) y_quad_fit = pr.predict(quadratic.fit_transform(X_fit))
Python
결과 그래프를 그린다.
plt.scatter(X, y, label='training points') plt.plot(X_fit, y_lin_fit, label='linear fit', linestyle='--') plt.plot(X_fit, y_quad_fit, label='quadratic fit') plt.legend(loc='upper left') plt.show()
Python
결과 이미지에서 볼 수 있듯 다항 회귀 모델이 선형 모델보다 특성과 타깃 사이의 관계를 훨씬 잘 잡아낸다.
y_lin_pred = lr.predict(X) y_quad_pred = pr.predict(X_quad) print('훈련 MSE 비교 - 선형 모델: %.3f, 다항 모델: %.3f' % (mean_squared_error(y, y_lin_pred), mean_squared_error(y, y_quad_pred))) # 훈련 MSE 비교 - 선형 모델: 569.780, 다항 모델: 61.330 print('훈련 R^2 비교 - 선형 모델: %.3f, 다항 모델: %.3f' % (r2_score(y, y_lin_pred), r2_score(y, y_quad_pred))) # 훈련 R^2 비교 - 선형 모델: 0.832, 다항 모델: 0.982
Python
코드 실행 결과를 보면 MSE는 선형 모델 570에서 다항 모델 61로 감소했고, 결정 계수를 보았을 때도 다항 모델 (0.982)이 선형 모델 (0.932)에 비해 더 잘 맞는다는 것을 보여준다.

주택 데이터셋을 사용한 비선형 관계 모델링

좀 더 실제적인 예를 들어보자.
X = df[['LSTAT']].values y = df['MEDV'].values regr = LinearRegression() # 이차, 삼차 다항식 특성을 만든다. quadratic = PolynomialFeatures(degree=2) cubic = PolynomialFeatures(degree=3) X_quad = quadratic.fit_transform(X) X_cubic = cubic.fit_transform(X) # 학습된 모델을 그리기 위해 특성 범위를 만든다. X_fit = np.arange(X.min(), X.max(), 1)[:, np.newaxis] regr = regr.fit(X, y) y_lin_fit = regr.predict(X_fit) linear_r2 = r2_score(y, regr.predict(X)) regr = regr.fit(X_quad, y) y_quad_fit = regr.predict(quadratic.fit_transform(X_fit)) quadratic_r2 = r2_score(y, regr.predict(X_quad)) regr = regr.fit(X_cubic, y) y_cubic_fit = regr.predict(cubic.fit_transform(X_fit)) cubic_r2 = r2_score(y, regr.predict(X_cubic)) # 결과 그래프를 그린다. plt.scatter(X, y, label='training points', color='lightgray') plt.plot(X_fit, y_lin_fit, label='linear (d=1), $R^2=%.2f$' % linear_r2, color='blue', lw=2, linestyle=':') plt.plot(X_fit, y_quad_fit, label='quadratic (d=2), $R^2=%.2f$' % quadratic_r2, color='red', lw=2, linestyle='-') plt.plot(X_fit, y_cubic_fit, label='cubic (d=3), $R^2=%.2f$' % cubic_r2, color='green', lw=2, linestyle='--') plt.xlabel('% lower status of the population [LSTAT]') plt.ylabel('Price in $1000s [MEDV]') plt.legend(loc='upper right') plt.show()
Python
위 결과 이미지에서 볼 수 있듯이 선형과 이차 다항 모델보다 삼차 다항 모델이 주택 가격과 LSTAT 사이의 관계를 잘 잡아냈다.
다항 특성을 많이 추가할수록 모델 복잡도가 높아지고 과대적합의 가능성이 증가한다는 사실을 기억하라.
실전에서는 별도의 테스트 세트에서 모델의 일반화 성능을 평가하는 것이 권장된다.
다항 특성이 비선형 관계를 모델링하는데 언제나 최선의 선택은 아니다.
예컨대 MEDV-LSTAT 산점도를 보면 경험과 직관을 바탕으로 로그 스케일로 변환한 LSTAT 특성과 MEDV의 제곱근은 선형 회귀 모델에 적합한 특성 공간을 구성하리라 추측할 수 있다.
필자 생각에 이런 두 변수 사이의 관계는 지수 함수와 매우 비슷할 것으로 추측된다.
f(x)=exf(x) = e^{-x}
지수 함수에 자연 로그를 취하면 직선이 되기 때문에 다음과 같은 로그 변환을 적용하는 것이 좋을 것 같다.
log(f(x))=x\log(f(x)) = -x
다음 코드를 실행하여 이런 가정을 테스트해 보자
# 특성을 변환한다. X_log = np.log(X) y_sqrt = np.sqrt(y) # 학습된 모델을 그리기 위해 특성 범위를 만든다. X_fit = np.arange(X_log.min()-1, X_log.max()+1, 1)[:, np.newaxis] regr = regr.fit(X_log, y_sqrt) y_lin_fit = regr.predict(X_fit) linear_r2 = r2_score(y_sqrt, regr.predict(X_log)) # 결과 그래프를 그린다. plt.scatter(X_log, y_sqrt, label='training points', color='lightgray') plt.plot(X_fit, y_lin_fit, label='linear (d=1), $R^2=%.2f$' % linear_r2, color='blue', lw=2, linestyle=':') plt.xlabel('log(% lower status of the population [LSTAT])') plt.ylabel('$\sqrt{Price \; in \; \$1000s \; [MEDV]}$') plt.legend(loc='upper left') plt.show()
Python
특성을 로그 스케일로 변환하고 타깃 값의 제곱근을 취하면 두 변수 사이에서 선형 회귀 관계를 잡을 수 있다.
앞서 만든 어떤 다항 특성 변환보다 이 데이터에서 학습한 선형 모델이 나아 보인다.

랜점 포레스트를 사용하여 비선형 관계 다루기

랜덤 포레스트 회귀는 다른 회귀 모델과는 개념적으로 다르다.
여러 개의 결정 트리를 앙상블한 랜덤 포레스트는 앞서 언급한 선형이나 다항 회귀 모델과 다르게 개별 선형 함수의 합으로 이해할 수 있다.
다른 말로 하면 결정 트리 알고리즘으로 입력 공간을 학습하기 좋은 더 작은 영역으로 분할한다.

결정 트리 회귀

결정 트리 알고리즘의 장점은 비선형 데이터를 다룰 때 특성 변환이 필요하지 않다는 점이다.
3장에서 보았듯이 결정 트리를 리프 노드가 순수 노드가 되거나 종료 기준을 만족할 때까지 반복적으로 노드를 분할한다.
분류에서 결정 트리를 사용할 때 정보 이득(IG)이 최대화되는 특성 분할을 결정하기 위해 불순도 지표로 엔트로피를 정의했다. 이진 분할에서는 정보 이득이 다음과 같이 정의 된다.
IG(Dp,xi)=I(Dp)NleftNpI(Dleft)NrightNpI(Dright)IG(D_{p}, x_{i}) = I(D_{p}) - {N_{left} \over N_{p}} I(D_{left}) - {N_{right} \over N_{p}} I(D_{right})
여기서 xx는 분할이 수행될 특성이다.
NpN_{p}는 부모 노드의 샘플 개수이다.
II는 불순도 함수이다.
DpD_{p}는 부모 노드에 있는 훈련 샘플 집합이고 DleftD_{left}와 DrightD_{right}는 분할 된 후 왼쪽 자식 노드와 오른쪽 자식 노드의 훈련 샘플 집합이다.
정보 이득을 최대화 하는 특성 분할을 찾는 것이 목적이다.
다른 말로 하면 자식 노드에서 불순도가 최대로 감소되는 특성 분할을 찾아야 한다.
분류에서 사용할 수 있는 불순도 지표로 지니 불순도와 엔트로피에 대해서는 3장에서 설명했다.
회귀 결정 트리를 사용하려면 연속적인 특성에 적합한 불순도 지표가 필요하므로 MSE를 노드 tt의 불순도 지표로 정의한다.
I(t)=MSE(t)=1NtiDt(y(i)y^t)2I(t) = MSE(t) = {1 \over N_{t}} \sum_{i \in D_{t}} (y^{(i)} - \hat{y}_{t})^{2}
여기서 NtN_{t}는 노드 tt에 있는 훈련 샘플 개수이다.
DtD_{t}는 노드 tt에 있는 훈련 샘플의 집합
y(i)y^{(i)}는 정보 타깃값, y^t\hat{y}_{t}는 예측된 타깃 값(샘플 평균)이다.
y^t=1NiDty(i)\hat{y}_{t} = {1 \over N} \sum_{i \in D_{t}} y^{(i)}
결정 트리 회귀에서는 MSE를 종종 노드 내 분산(within-node variance)이라고도 한다.
이런 이유로 분할 기준을 분산 감소(variance reduction)라고 많이 부른다.
사이킷런에 구현된 DecisionTreeRegressor로 MEDV와 LSTAT 변수 간의 비선형 관계를 모델링하여 결정 트리가 어떤 직선을 학습하는지 알아보자.
from sklearn.tree import DecisionTreeRegressor tree = DecisionTreeRegressor(max_depth=3) tree.fit(X, y) sort_idx = X.flatten().argsort() lin_regplot(X[sort_idx], y[sort_idx], tree) plt.xlabel('log(% lower status of the population [LSTAT])') plt.ylabel('Price in $1000s [MEDV]') plt.show()
Python
결과 그래프에서 볼 수 있듯이 결정 트리는 데이터에 있는 일반적인 경향을 잡아낸다.
기대하는 예측이 연속적이고 매끄러운 경우를 나타내지 못하는 것이 이 모델의 한계이다.
또 데이터 과대적합되거나 과소적합이 되지 않도록 적절한 트리의 깊이를 주의 깊게 선택해야 한다.

랜점 포레스트 회귀

랜덤 포레스트 알고리즘은 여러 개의 결정 트리를 연결하는 앙상블 방법이다.
랜덤 포레스트는 일반적으로 단일 결정 트리보다 더 나은 일반화 성능을 낸다. 무작위성이 모델의 분산을 낮추어 주기 때문이다.
랜덤 포레스트의 다른 장점은 데이터셋에 있는 이상치에 덜 민감하고 하이퍼파라미터 튜닝이 많이 필요하지 않다는 것이다. 일반적으로 랜덤 포레스트에서 튜닝할 하이퍼파라미터는 앙상블의 트리 개수이다.
회귀를 위한 기본적인 랜덤 포레스트 알고리즘은 3장에서 본 분류를 위한 랜덤 포레스트 알고리즘과 거의 동일하다. 유일한 차이점은 개별 결정 트리를 성장시키기 위해 MSE 기준을 사용하는 것이다.
타깃 값의 예측은 모든 결정 트리의 예측을 평균하여 계산한다.
주택 데이터셋에 있는 모든 특성을 사용하여 랜덤 포레스트 회귀 모델을 훈련시켜 보자.
샘플의 60%로 훈련하고 40%로 성능 평가를 해보겠다.
X = df.iloc[:, :-1].values y = df['MEDV'].values X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=1) forest = RandomForestRegressor(n_estimators=1000, criterion='mse', random_state=1, n_jobs=-1) forest.fit(X_train, y_train) y_train_pred = forest.predict(X_train) y_test_pred = forest.predict(X_test) print('훈련 MSE: %.3f, 테스트 MSE: %.3f' % (mean_squared_error(y_train, y_train_pred), mean_squared_error(y_test, y_test_pred))) # 훈련 MSE: 1.641, 테스트 MSE: 11.056 print('훈련 R^2: %.3f, 테스트 R^2: %.3f' % (r2_score(y_train, y_train_pred), r2_score(y_test, y_test_pred))) # 훈련 R^2: 0.979, 테스트 R^2: 0.878
Python
결과에서 랜덤 포레스트는 훈련 데이터에 과대적합되는 경향을 볼 수 있다. 하지만 여전히 타깃과 특성 간의 관계를 비교적 잘 설명하고 있다.
마지막으로 예측 잔차를 확인해 보자.
plt.scatter(y_train_pred, y_train_pred - y_train, c='steelblue', edgecolor='white', marker='o', s=35, alpha=0.9, label='training data') plt.scatter(y_test_pred, y_test_pred - y_test, c='limegreen', edgecolor='white', marker='s', s=35, alpha=0.9, label='test data') plt.xlabel('Predicted values') plt.ylabel('Residuals') plt.legend(loc='upper left') plt.hlines(y=0, xmin=-10, xmax=50, lw=2, color='black') plt.xlim([-10, 50]) plt.tight_layout() plt.show()
Python
이미 R2R^{2} 결정 계수에서 나타났듯이 모델이 테스트 데이터보다 훈련 데이터에 잘 맞는다.
잔차 그래프를 보면 y축 방향으로 테스트 세트에 이상치가 보이고, 잔차의 분포가 0을 중심으로 완전히 랜덤하지 않아 보인다. 이는 모델이 특성의 정보를 모두 잡아낼 수 없다는 것을 나타낸다.
그렇더라도 이 잔차 그래프는 이전 장에서 만든 선형 모델의 잔차 그래프에 비해 많이 개선된 것이다.
이상적으로 모델의 오차가 랜덤하거나 예측할 수 없어야 한다. 다른 말로 하면 예측 오차가 특성에 담긴 어떤 정보와도 관계가 없어야 한다.
현실 세계에 있는 분포나 패턴의 무작위성을 반영해야 한다.
잔차 그래프를 조사하여 예측 오차에 패턴이 감지되면 잔차 그래프가 예측 정보를 담고 있다는 의미이다. 일반적으로 특성의 정보가 잔차로 누설되는 것이 원인이다.
안타깝지만 랜덤하지 않은 잔차 그래프 문제를 다루기 위한 보편적인 방법은 없으며 실험을 해보아야 안다.
가용한 데이터에 따라 특성을 변환하거나 학습 알고리즘의 하이퍼파라미터를 튜닝하여 모델을 향상시킬 수 있다.
또는 더 간단하거나 더 복잡한 모델을 선택할 수 있고, 이상치를 제거하거나 추가적인 특성을 포함하여 모델의 성능을 높일 수 있다.