# 22.01.27 머신러닝 스터디 5장. 회귀 - 2
<파이썬 머신러닝 완벽 가이드>¶
회귀 2 ( p319 ~ p376)¶
따라서 회귀 계수의 크기를 제어해 과적합을 개선하려면 비용 함수의 목표가 위와 같이 변경될 수 있다. 이 때 alpha는 학습 데이터 적합 정도와 회귀 계수 값의 크기 제어를 수행하는 튜닝 파라미터로, 만약 alpha가 0이라면 비용 함수 식은 기존과 동일한 Min(RSS(W))가 될 것이고, 만약 alpha가 무한대라면 비용 함수 식은 W값을 매우 작게 만들어야 Cost가 최소화 되는 비용 함수 목표를 달성할 수 있다. 즉, alpha 값을 크게 하면 비용 함수는 회귀 계수 W 값을 작게 해 과적합을 개선할 수 있고 alpha 값을 작게 하면 회귀 계수 W의 값이 커져도 어느 정도 상쇄가 가능하다.
-> alpha를 0에서부터 지속적으로 값을 증가시키면 회귀 계수 값의 크기를 감소시킬 수 있다. 이처럼 alpha 값으로 페널티를 부여해 회귀 계수 값의 크기를 감소시켜 과적합을 개선하는 방식을 규제(Regularizatiaon)라고 한다.
L2규제는 위에서 말한 것 처럼 W의 제곱에 대해 페널티를 부여하는 방식을 말한다. L2규제를 적용한 회귀를 릿지(Ridge) 회귀라고 부른다.
L1규제는 alpha * ||W||와 같이 W의 절댓값에 대해 페널티를 부여한다. L1규제를 적용한 회귀를 라쏘(Lasso) 회귀라고 부르며 L1규제를 적용하면 영향력이 크지 않은 회귀 계수 값을 0으로 변환한다.
# LinearRegression 예제에서 사용한 피처 데이터 세트
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from scipy import stats
from sklearn.datasets import load_boston
%matplotlib inline
# boston 데이터 세트 로드
boston = load_boston()
# boston 데이터 세트 DataFrame 변환
bostonDF = pd.DataFrame(boston.data, columns = boston.feature_names)
# boston 데이터 세트의 target 배열은 주택 가격. 이를 Price 칼럼으로 DataFrame에 추가함
bostonDF['PRICE'] = boston.target
y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'],axis=1,inplace=False)
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
# alpha = 10으로 설정해 릿지 회귀 수행
ridge = Ridge(alpha = 10)
neg_mse_scores = cross_val_score(ridge, X_data, y_target, scoring = "neg_mean_squared_error", cv = 5)
rmse_scores = np.sqrt(-1 * neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
print(' 5 folds 의 개별 Negative MSE scores: ', np.round(neg_mse_scores, 2))
print(' 5 folds 의 개별 RMSE scores : ', np.round(rmse_scores, 2))
print(' 5 folds 의 평균 RMSE : {0:.3f} '.format(avg_rmse))
# LinearRegression의 RMSE 평균인 5.829보다 성능이 좋다~!!
5 folds 의 개별 Negative MSE scores: [-11.42 -24.29 -28.14 -74.6 -28.52] 5 folds 의 개별 RMSE scores : [3.38 4.93 5.31 8.64 5.34] 5 folds 의 평균 RMSE : 5.518
# alpha 값을 변화 시키면서 달라지는 RMSE 평균값을 반환하는 코드
alphas = [0, 0.1, 1, 10, 100]
# alphas list 값을 반복하면서 alpha에 따른 평균 rmse를 구함
for alpha in alphas:
ridge = Ridge(alpha = alpha)
# cross_val_score
neg_mse_scores = cross_val_score(ridge, X_data, y_target, scoring = "neg_mean_squared_error", cv = 5)
avg_rmse = np.mean(np.sqrt(-1 * neg_mse_scores))
print(f'alpha = {alpha} 일 때 5 folds 의 평균 RMSE : {round(avg_rmse,3)}')
alpha = 0 일 때 5 folds 의 평균 RMSE : 5.829 alpha = 0.1 일 때 5 folds 의 평균 RMSE : 5.788 alpha = 1 일 때 5 folds 의 평균 RMSE : 5.653 alpha = 10 일 때 5 folds 의 평균 RMSE : 5.518 alpha = 100 일 때 5 folds 의 평균 RMSE : 5.33
# alpha 값의 변화에 따른 피처의 회귀 계수 값을 시각화
# 각 alpha에 따른 회귀 계수 값을 시각화
fig , axs = plt.subplots(figsize=(18,6) , nrows=1 , ncols=5)
# 각 alpha에 따른 회귀 계수 값을 데이터로 저장하기 위한 DataFrame 생성
coeff_df = pd.DataFrame()
# alphas 리스트 값을 차례로 입력해 회귀 계수 값 시각화 및 데이터 저장. pos는 axis의 위치 지정
for pos , alpha in enumerate(alphas) :
ridge = Ridge(alpha = alpha)
ridge.fit(X_data , y_target)
# alpha에 따른 피처별 회귀 계수를 Series로 변환하고 이를 DataFrame의 컬럼으로 추가.
coeff = pd.Series(data=ridge.coef_ , index=X_data.columns )
colname='alpha:'+str(alpha)
coeff_df[colname] = coeff
# 막대 그래프로 각 alpha 값에서의 회귀 계수를 시각화. 회귀 계수값이 높은 순으로 표현
coeff = coeff.sort_values(ascending=False)
axs[pos].set_title(colname)
axs[pos].set_xlim(-3,6)
sns.barplot(x=coeff.values , y=coeff.index, ax=axs[pos])
# for 문 바깥에서 맷플롯립의 show 호출 및 alpha에 따른 피처별 회귀 계수를 DataFrame으로 표시
plt.show()
alpha값을 증가시킬 수록 전체적인 회귀 계수가 작아지는 것을 확인할 수 있다.
# 각 alpha에 따른 회귀 계수 값
coeff_df.sort_values(by = 'alpha:0', ascending = False)
alpha:0 | alpha:0.1 | alpha:1 | alpha:10 | alpha:100 | |
---|---|---|---|---|---|
RM | 3.809865 | 3.818233 | 3.854000 | 3.702272 | 2.334536 |
CHAS | 2.686734 | 2.670019 | 2.552393 | 1.952021 | 0.638335 |
RAD | 0.306049 | 0.303515 | 0.290142 | 0.279596 | 0.315358 |
ZN | 0.046420 | 0.046572 | 0.047443 | 0.049579 | 0.054496 |
INDUS | 0.020559 | 0.015999 | -0.008805 | -0.042962 | -0.052826 |
B | 0.009312 | 0.009368 | 0.009673 | 0.010037 | 0.009393 |
AGE | 0.000692 | -0.000269 | -0.005415 | -0.010707 | 0.001212 |
TAX | -0.012335 | -0.012421 | -0.012912 | -0.013993 | -0.015856 |
CRIM | -0.108011 | -0.107474 | -0.104595 | -0.101435 | -0.102202 |
LSTAT | -0.524758 | -0.525966 | -0.533343 | -0.559366 | -0.660764 |
PTRATIO | -0.952747 | -0.940759 | -0.876074 | -0.797945 | -0.829218 |
DIS | -1.475567 | -1.459626 | -1.372654 | -1.248808 | -1.153390 |
NOX | -17.766611 | -16.684645 | -10.777015 | -2.371619 | -0.262847 |
하지만 L2규제(Ridge)는 회귀 계수를 0으로 만들지 않는다.
라쏘 회귀¶
# alpha 값을 변화시키면서 RMSE와 각 피처의 회귀 계수를 출력
from sklearn.linear_model import Lasso, ElasticNet
# alpha값에 따른 회귀 모델의 폴드 평균 RMSE를 출력하고 회귀 계수값들을 DataFrame으로 반환
def get_linear_reg_eval(model_name, params=None, X_data_n=None, y_target_n=None, verbose=True, return_coeff = True):
coeff_df = pd.DataFrame()
if verbose : print('####### ', model_name , '#######')
for param in params:
if model_name =='Ridge': model = Ridge(alpha=param)
elif model_name =='Lasso': model = Lasso(alpha=param)
elif model_name =='ElasticNet': model = ElasticNet(alpha=param, l1_ratio=0.7) # l1_ratio를 고정해두었다는 것을 확인!!
neg_mse_scores = cross_val_score(model, X_data_n,
y_target_n, scoring="neg_mean_squared_error", cv = 5)
avg_rmse = np.mean(np.sqrt(-1 * neg_mse_scores))
print('alpha {0}일 때 5 폴드 세트의 평균 RMSE: {1:.3f} '.format(param, avg_rmse))
# cross_val_score는 evaluation metric만 반환하므로 모델을 다시 학습하여 회귀 계수 추출
model.fit(X_data , y_target)
# alpha에 따른 피처별 회귀 계수를 Series로 변환하고 이를 DataFrame의 컬럼으로 추가.
coeff = pd.Series(data=model.coef_ , index=X_data.columns )
colname='alpha:'+str(param)
coeff_df[colname] = coeff
return coeff_df
# 라쏘에 사용될 alpha 파라미터의 값을 정의
lasso_alphas = [0.07, 0.1, 0.5, 1, 3]
coeff_lasso_df = get_linear_reg_eval('Lasso', params = lasso_alphas, X_data_n = X_data, y_target_n = y_target)
# 0.07일 때가 가장 좋다!
####### Lasso ####### alpha 0.07일 때 5 폴드 세트의 평균 RMSE: 5.612 alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.615 alpha 0.5일 때 5 폴드 세트의 평균 RMSE: 5.669 alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.776 alpha 3일 때 5 폴드 세트의 평균 RMSE: 6.189
# 반환된 coeff_lasso_df를 첫번째 컬럼순으로 내림차순 정렬하여 회귀계수 DataFrame출력
sort_column = 'alpha:'+str(lasso_alphas[0])
coeff_lasso_df.sort_values(by=sort_column, ascending=False)
alpha:0.07 | alpha:0.1 | alpha:0.5 | alpha:1 | alpha:3 | |
---|---|---|---|---|---|
RM | 3.789725 | 3.703202 | 2.498212 | 0.949811 | 0.000000 |
CHAS | 1.434343 | 0.955190 | 0.000000 | 0.000000 | 0.000000 |
RAD | 0.270936 | 0.274707 | 0.277451 | 0.264206 | 0.061864 |
ZN | 0.049059 | 0.049211 | 0.049544 | 0.049165 | 0.037231 |
B | 0.010248 | 0.010249 | 0.009469 | 0.008247 | 0.006510 |
NOX | -0.000000 | -0.000000 | -0.000000 | -0.000000 | 0.000000 |
AGE | -0.011706 | -0.010037 | 0.003604 | 0.020910 | 0.042495 |
TAX | -0.014290 | -0.014570 | -0.015442 | -0.015212 | -0.008602 |
INDUS | -0.042120 | -0.036619 | -0.005253 | -0.000000 | -0.000000 |
CRIM | -0.098193 | -0.097894 | -0.083289 | -0.063437 | -0.000000 |
LSTAT | -0.560431 | -0.568769 | -0.656290 | -0.761115 | -0.807679 |
PTRATIO | -0.765107 | -0.770654 | -0.758752 | -0.722966 | -0.265072 |
DIS | -1.176583 | -1.160538 | -0.936605 | -0.668790 | -0.000000 |
엘라스틱넷 회귀¶
$$RSS(W) + alpha2 * \left\| W\right\|^{2}_2 + alpha1 * \left\| W\right\|_1$$엘라스틱넷(Elastic Net)회귀는 L2 규제와 L1 규제를 결합한 회귀이다. 따라서 비용함수의 목표는 위의 식을 최소화 하는 W를 찾는 것이다. 엘라스틱넷은 라쏘 회귀의 서로 상관관계가 높은 피처들의 경우 이들 중에서 중요 피처만을 셀렉션하고 다른 피처들의 회귀 계수를 모두 0으로 만드는 성향으로 인해 급격히 변동하는 회귀 계수의 값을 완화하기 위해 L2 규제를 라쏘 회귀에 추가한 것이다. 단점으로는 L1과 L2 규제가 모두 결합된 탓에 수행시간이 상대적으로 오래 걸린다는 점이다.
ElasticNet 클래스의 주요 파라미터는 alpha와 l1_ratio로 여기서 alpha의 의미는 Ridge나 Lasso 클래스에서의 alpha의 의미와는 다르다. 엘라스틱넷의 규제는 a L1 + b L2로 정의될 수 있으며 이 때 a는 L1 규제, b는 L2규제의 alpha값이므로 ElasticNet 클래스의 alpha 파라미터 값은 a + b이다. 또한 l1_ratio 파라미터의 값은 a / (a + b)로 l1_ratio가 0일 경우 a = 0이므로 L2규제와 동일하고 l1_ratio = 1이면 b = 0이므로 L1 규제와 동일하다.
# 엘라스틱넷에 사용될 alpha 파라미터의 값들을 정의하고 get_linear_reg_eval() 함수 호출
# l1_ratio는 0.7로 고정
elastic_alphas = [ 0.07, 0.1, 0.5, 1, 3]
coeff_elastic_df =get_linear_reg_eval('ElasticNet', params=elastic_alphas,
X_data_n = X_data, y_target_n = y_target)
####### ElasticNet ####### alpha 0.07일 때 5 폴드 세트의 평균 RMSE: 5.542 alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.526 alpha 0.5일 때 5 폴드 세트의 평균 RMSE: 5.467 alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.597 alpha 3일 때 5 폴드 세트의 평균 RMSE: 6.068
# 반환된 coeff_elastic_df를 첫번째 컬럼순으로 내림차순 정렬하여 회귀계수 DataFrame출력
sort_column = 'alpha:'+str(elastic_alphas[0])
coeff_elastic_df.sort_values(by=sort_column, ascending=False)
alpha:0.07 | alpha:0.1 | alpha:0.5 | alpha:1 | alpha:3 | |
---|---|---|---|---|---|
RM | 3.574162 | 3.414154 | 1.918419 | 0.938789 | 0.000000 |
CHAS | 1.330724 | 0.979706 | 0.000000 | 0.000000 | 0.000000 |
RAD | 0.278880 | 0.283443 | 0.300761 | 0.289299 | 0.146846 |
ZN | 0.050107 | 0.050617 | 0.052878 | 0.052136 | 0.038268 |
B | 0.010122 | 0.010067 | 0.009114 | 0.008320 | 0.007020 |
AGE | -0.010116 | -0.008276 | 0.007760 | 0.020348 | 0.043446 |
TAX | -0.014522 | -0.014814 | -0.016046 | -0.016218 | -0.011417 |
INDUS | -0.044855 | -0.042719 | -0.023252 | -0.000000 | -0.000000 |
CRIM | -0.099468 | -0.099213 | -0.089070 | -0.073577 | -0.019058 |
NOX | -0.175072 | -0.000000 | -0.000000 | -0.000000 | -0.000000 |
LSTAT | -0.574822 | -0.587702 | -0.693861 | -0.760457 | -0.800368 |
PTRATIO | -0.779498 | -0.784725 | -0.790969 | -0.738672 | -0.423065 |
DIS | -1.189438 | -1.173647 | -0.975902 | -0.725174 | -0.031208 |
상황에 따라 릿지, 라쏘, 엘라스틱넷 중 어떤 것이 좋은지가 달라진다. 또한 선형 회귀의 경우 하이퍼 파라미터를 찾는 것뿐만 아니라 데이터 분포도의 정규화와 인코딩 방법이 매우 중요하다.
선형 회귀 모델을 위한 데이터 변환¶
선형 회귀 모델 : 일반적으로 피처와 타깃값 간에 선형의 관계가 있다고 가정하고, 최적의 선형함수를 찾아내 결과값을 예측한다. 또한 선형 회귀 모델은 정규 분포 형태를 매우 선호한다. 따라서 선형 회귀 모델을 적용하기 전에 먼저 데이터에 대한 스케일링 / 정규화 작업을 수행하는 것이 일반적이다.
StandardScaler 클래스로 평균이 0, 분산이 1인 표준 정규 분포 데이터 세트로 변환, MinMaxScaler 클래스를 이용해 최솟값이 0이고 최댓값이 1인 값으로 정규화를 수행
스케일링/ 정규화를 수행한 데이터 세트에 다시 다항 특성을 적용하여 변환하는 방법. 보통 1번 방법으로 성능 향상이 없을 경우 사용
원래 값에 log 함수를 적용하면 보다 정규 분포에 가까운 형태로 값이 분포된다. ( 로그 변환 ) 1번 방법의 경우 예측 성능 향상을 크게 기대하기 어려운 경우가 많으며 2번 방법의 경우 피처의 개수가 매우 많을 경우에는 다항 변환으로 생성되는 피처의 개수가 기하급수적으로 늘어나서 과적합의 이슈가 발생할 수 있기 때문에 로그 변환을 자주 사용한다.
타깃값의 경우는 일반적으로 로그 변환을 사용한다.
보스턴 주택가격 데이터로 표준 정규 분포 변환, 최대값/최솟값 정규화, 로그 변환 후 RMSE 성능 평가
# method는 표준 정규 분포 변환(Standard), 최대값/최소값 정규화(MinMax), 로그변환(Log) 결정
# p_degree는 다향식 특성을 추가할 때 적용. p_degree는 2이상 부여하지 않음.
from sklearn.preprocessing import StandardScaler, MinMaxScaler, PolynomialFeatures
def get_scaled_data(method='None', p_degree=None, input_data=None):
if method == 'Standard':
scaled_data = StandardScaler().fit_transform(input_data)
elif method == 'MinMax':
scaled_data = MinMaxScaler().fit_transform(input_data)
elif method == 'Log':
scaled_data = np.log1p(input_data)
else:
scaled_data = input_data
if p_degree != None:
scaled_data = PolynomialFeatures(degree=p_degree,
include_bias=False).fit_transform(scaled_data)
return scaled_data
5가지의 피처 데이터 변환 방법 : 원본 데이터, 표준 정규 분포, 표준 정규 분포 - 2차 다항식 변환, MinMax 정규화, MinMax정규화- 2차 다항식 변환, 로그 변환
# Ridge의 alpha값을 다르게 적용하고 다양한 데이터 변환방법에 따른 RMSE 추출.
alphas = [0.1, 1, 10, 100]
scale_methods=[(None, None), ('Standard', None), ('Standard', 2),
('MinMax', None), ('MinMax', 2), ('Log', None)]
for scale_method in scale_methods:
X_data_scaled = get_scaled_data(method=scale_method[0], p_degree=scale_method[1],
input_data=X_data)
print('\n## 변환 유형:{0}, Polynomial Degree:{1}'.format(scale_method[0], scale_method[1]))
get_linear_reg_eval('Ridge', params=alphas, X_data_n=X_data_scaled,
y_target_n=y_target, verbose=False)
## 변환 유형:None, Polynomial Degree:None alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.788 alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.653 alpha 10일 때 5 폴드 세트의 평균 RMSE: 5.518 alpha 100일 때 5 폴드 세트의 평균 RMSE: 5.330 ## 변환 유형:Standard, Polynomial Degree:None alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.826 alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.803 alpha 10일 때 5 폴드 세트의 평균 RMSE: 5.637 alpha 100일 때 5 폴드 세트의 평균 RMSE: 5.421 ## 변환 유형:Standard, Polynomial Degree:2 alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 8.827 alpha 1일 때 5 폴드 세트의 평균 RMSE: 6.871 alpha 10일 때 5 폴드 세트의 평균 RMSE: 5.485 alpha 100일 때 5 폴드 세트의 평균 RMSE: 4.634 ## 변환 유형:MinMax, Polynomial Degree:None alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.764 alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.465 alpha 10일 때 5 폴드 세트의 평균 RMSE: 5.754 alpha 100일 때 5 폴드 세트의 평균 RMSE: 7.635 ## 변환 유형:MinMax, Polynomial Degree:2 alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.298 alpha 1일 때 5 폴드 세트의 평균 RMSE: 4.323 alpha 10일 때 5 폴드 세트의 평균 RMSE: 5.185 alpha 100일 때 5 폴드 세트의 평균 RMSE: 6.538 ## 변환 유형:Log, Polynomial Degree:None alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 4.770 alpha 1일 때 5 폴드 세트의 평균 RMSE: 4.676 alpha 10일 때 5 폴드 세트의 평균 RMSE: 4.836 alpha 100일 때 5 폴드 세트의 평균 RMSE: 6.241
다항식 변환은 피처의 개수가 많을 경우 적용하기 힘들며, 데이터 건수가 많아지면 계산에 많은 시간이 소모되어 적용에 한계가 있다.
이처럼 데이터 세트에 데이터 값의 분포가 심하게 왜곡되어 있을 경우에는 로그 변환을 적용하는 것이 좋다.
7. 로지스틱 회귀¶
로지스틱 회귀는 선형 회귀 방식을 분류에 적용한 알고리즘이다. 회귀가 선형인가 비선형인가는 독립변수가 아닌 가중치 변수가 선형인지 아닌지를 따르기 때문에 로지스틱 회귀는 선형 회귀 계열이다. 로지스틱 회귀가 선형 회귀와 다른 점은 학습을 통해 선형 함수의 회귀 최적선을 찾는 것이 아니라 시그모이드 함수의 최적선을 찾고 이 시그모이드 함수의 반환 값을 확률로 간주해 확률에 따라 분류를 결정한다는 것이다.
$$y = \frac{1}{1+e^{-x}}$$
시그모이드 함수의 정의는 위와 같고 여기서 알 수 있듯이 시그모이드 함수는 x 값이 아무리 커지거나 작아져도 y값은 항상 0 ~ 1을 반환한다. x 값이 커지면 1에 근사하고 x 값이 작아지면 0에 근사한다. ( x = 0 일때는 0.5 ) ( 선형 회귀 방식을 기반으로 시그모이드 함수를 이용해 분류를 수행 )
하이퍼 파라미터
- penalty : 규제의 유형을 설정, l1, l2로 설정 가능, default = l2
- C : 규제 강도를 조절하는 alpha 값의 역수, C 값이 작을 수록 규제 강도가 크다
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
cancer = load_breast_cancer()
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
# StandardScaler( )로 평균이 0, 분산 1로 데이터 분포도 변환
scaler = StandardScaler()
data_scaled = scaler.fit_transform(cancer.data)
X_train , X_test, y_train , y_test = train_test_split(data_scaled, cancer.target, test_size=0.3, random_state=0)
from sklearn.metrics import accuracy_score, roc_auc_score
# 로지스틱 회귀를 이용하여 학습 및 예측 수행.
lr_clf = LogisticRegression()
lr_clf.fit(X_train, y_train)
lr_preds = lr_clf.predict(X_test)
# accuracy와 roc_auc 측정
print('accuracy: {:0.3f}'.format(accuracy_score(y_test, lr_preds)))
print('roc_auc: {:0.3f}'.format(roc_auc_score(y_test , lr_preds)))
accuracy: 0.977 roc_auc: 0.972
from sklearn.model_selection import GridSearchCV
params={'penalty':['l2'], # penalty가 오직 l2와 none만 가질 수 있다.
'C':[0.01, 0.1, 1, 1, 5, 10]}
grid_clf = GridSearchCV(lr_clf, param_grid=params, scoring='accuracy', cv=3 )
grid_clf.fit(data_scaled, cancer.target)
print('최적 하이퍼 파라미터:{0}, 최적 평균 정확도:{1:.3f}'.format(grid_clf.best_params_,
grid_clf.best_score_))
최적 하이퍼 파라미터:{'C': 1, 'penalty': 'l2'}, 최적 평균 정확도:0.975
로지스틱 회귀는 가볍고 빠르며 이진 분류 예측 성능도 뛰어나다. 또한 희소한 데이터 세트 분류에도 뛰어나 텍스트 분류에서 자주 사용된다.
8. 회귀 트리¶
회귀를 위한 트리를 생성하고 이를 기반으로 회귀 예측을 한다는 점에서는 분류 트리와 크게 다르지 않지만 리프 노드에서 예측 결정 값을 만드는 과정에서의 차이가 존재한다. 분류 트리는 특정 클래스 레이블을 결정하는 것과 달리 회귀 트리는 리프 노드에 속한 데이터 값의 평균값을 구해 회귀 예측값을 계산한다.
DT, RF, GBM, XGBoost, LightGBm 등의 모든 트리 계열 알고리즘은 회귀도 가능하다.
from sklearn.datasets import load_boston
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor
import pandas as pd
import numpy as np
# 보스턴 데이터 세트 로드
boston = load_boston()
bostonDF = pd.DataFrame(boston.data, columns = boston.feature_names)
bostonDF['PRICE'] = boston.target
y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'], axis=1,inplace=False)
rf = RandomForestRegressor(random_state=0, n_estimators=1000)
neg_mse_scores = cross_val_score(rf, X_data, y_target, scoring="neg_mean_squared_error", cv = 5)
rmse_scores = np.sqrt(-1 * neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
print(' 5 교차 검증의 개별 Negative MSE scores: ', np.round(neg_mse_scores, 2))
print(' 5 교차 검증의 개별 RMSE scores : ', np.round(rmse_scores, 2))
print(' 5 교차 검증의 평균 RMSE : {0:.3f} '.format(avg_rmse))
5 교차 검증의 개별 Negative MSE scores: [ -7.88 -13.14 -20.57 -46.23 -18.88] 5 교차 검증의 개별 RMSE scores : [2.81 3.63 4.54 6.8 4.34] 5 교차 검증의 평균 RMSE : 4.423
# 입력 모델과 데이터 세트를 입력 받아 교차 검증으로 평균 RMSE를 계싼해주는 함수
def get_model_cv_prediction(model, X_data, y_target):
neg_mse_scores = cross_val_score(model, X_data, y_target, scoring="neg_mean_squared_error", cv = 5)
rmse_scores = np.sqrt(-1 * neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
print('##### ',model.__class__.__name__ , ' #####')
print(' 5 교차 검증의 평균 RMSE : {0:.3f} '.format(avg_rmse))
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import GradientBoostingRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
dt_reg = DecisionTreeRegressor(random_state=0, max_depth=4)
rf_reg = RandomForestRegressor(random_state=0, n_estimators=1000)
gb_reg = GradientBoostingRegressor(random_state=0, n_estimators=1000)
xgb_reg = XGBRegressor(n_estimators=1000)
lgb_reg = LGBMRegressor(n_estimators=1000)
# 트리 기반의 회귀 모델을 반복하면서 평가 수행
models = [dt_reg, rf_reg, gb_reg, xgb_reg, lgb_reg]
for model in models:
get_model_cv_prediction(model, X_data, y_target)
##### DecisionTreeRegressor ##### 5 교차 검증의 평균 RMSE : 5.978 ##### RandomForestRegressor ##### 5 교차 검증의 평균 RMSE : 4.423 ##### GradientBoostingRegressor ##### 5 교차 검증의 평균 RMSE : 4.269 ##### XGBRegressor ##### 5 교차 검증의 평균 RMSE : 4.251 ##### LGBMRegressor ##### 5 교차 검증의 평균 RMSE : 4.646
# Regressor 클래스는 선형 회귀와는 다른 처리 방식이기 때문에 회귀 계수를 제공하는 coef_ 속성이 없다. 대신 피처 중요도를 확인 가능
import seaborn as sns
%matplotlib inline
rf_reg = RandomForestRegressor(n_estimators=1000)
rf_reg.fit(X_data, y_target)
feature_series = pd.Series(data=rf_reg.feature_importances_, index=X_data.columns ) # linearRegression의 경우에는 모델.coef_를 사용!
feature_series = feature_series.sort_values(ascending=False)
sns.barplot(x= feature_series, y=feature_series.index)
plt.show()
회귀 트리 Regressor의 하이퍼 파라미터는 분류 트리 Classifier의 하이퍼 파라미터와 거의 동일하다.
# 2차원 평면상에서 회귀 예측선을 쉽게 표현하기 위해 1개의 변수만 추출
import matplotlib.pyplot as plt
%matplotlib inline
bostonDF_sample = bostonDF[['RM','PRICE']]
bostonDF_sample = bostonDF_sample.sample(n=100,random_state=0)
print(bostonDF_sample.shape)
plt.figure()
plt.scatter(bostonDF_sample.RM , bostonDF_sample.PRICE,c="darkorange")
plt.show()
(100, 2)
# LinearRegression, DecisionTreeRegressor
import numpy as np
from sklearn.linear_model import LinearRegression
# 선형 회귀와 결정 트리 기반의 Regressor 생성. DecisionTreeRegressor의 max_depth는 각각 2, 7
lr_reg = LinearRegression()
rf_reg2 = DecisionTreeRegressor(max_depth=2)
rf_reg7 = DecisionTreeRegressor(max_depth=7)
# 실제 예측을 적용할 테스트용 데이터 셋을 4.5 ~ 8.5 까지 100개 데이터 셋 생성.
X_test = np.arange(4.5, 8.5, 0.04).reshape(-1, 1)
# 보스턴 주택가격 데이터에서 시각화를 위해 피처는 RM만, 그리고 결정 데이터인 PRICE 추출
X_feature = bostonDF_sample['RM'].values.reshape(-1,1)
y_target = bostonDF_sample['PRICE'].values.reshape(-1,1)
# 학습과 예측 수행.
lr_reg.fit(X_feature, y_target)
rf_reg2.fit(X_feature, y_target)
rf_reg7.fit(X_feature, y_target)
pred_lr = lr_reg.predict(X_test)
pred_rf2 = rf_reg2.predict(X_test)
pred_rf7 = rf_reg7.predict(X_test)
fig , (ax1, ax2, ax3) = plt.subplots(figsize=(14,4), ncols=3)
# X축값을 4.5 ~ 8.5로 변환하며 입력했을 때, 선형 회귀와 결정 트리 회귀 예측 선 시각화
# 선형 회귀로 학습된 모델 회귀 예측선
ax1.set_title('Linear Regression')
ax1.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
ax1.plot(X_test, pred_lr,label="linear", linewidth=2 )
# DecisionTreeRegressor의 max_depth를 2로 했을 때 회귀 예측선
ax2.set_title('Decision Tree Regression: \n max_depth=2')
ax2.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
ax2.plot(X_test, pred_rf2, label="max_depth:3", linewidth=2 )
# DecisionTreeRegressor의 max_depth를 7로 했을 때 회귀 예측선
ax3.set_title('Decision Tree Regression: \n max_depth=7')
ax3.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
ax3.plot(X_test, pred_rf7, label="max_depth:7", linewidth=2)
plt.show()
선형 회귀는 직선으로 예측 회귀선을 표현, 회귀 트리의 경우에는 분할되는 데이터 지점에 따라 브랜치를 만들면서 계단 형태로 회귀선을 만든다.
9. 자전거 대여 수요 예측¶
구체적인 내용은 생략
# RMSLE
from sklearn.metrics import mean_squared_error, mean_absolute_error
def rmse(y, pred):
log_y = np.log1p(y)
log_pred = np.log1p(pred)
squared_error = (log_y - log_pred) ** 2
rmsle = np.sqrt(np.mean(squared_error))
return rmsle
log1p 함수를 사용하는 이유는 log 함수를 이용할 때 데이터 값의 크기에 따라 오버플로 / 언더플로 오류가 발생하기 쉽기 때문이다.
# RMSE
def rmse(y, pred):
return np.sqrt(mean_squared_error(y, pred))
# 예측 값과 실제 값이 얼마나 차이나는 지 확인
def get_top_error_data(y_test, pred, n_tops = 5):
# DataFrame의 칼럼으로 실제값과 예측값을 서로 비교할 수 있도록 생성
result_df = pd.DataFrame(y_test.values, columns = ['실제값'])
result_df['예측값'] = np.round(pred)
result_df['diff'] = np.abs(result_df['실제값'] - result_df['예측값'])
# 예측값과 실제 값의 차이가 가장 큰 데이터 순으로 출력
print(result_df.sort_values('diff', ascending = False)[:n_tops])
numpy.expm1 함수는 입력 어레이의 값에 대해 exp(x) - 1을 계산한다.
10. 캐글 주택 가격 : 고급 회귀 기법¶
데이터 사전 처리
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
house_df_org = pd.read_csv('./house_price.csv')
house_df = house_df_org.copy()
house_df.head(3)
Id | MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | ... | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | SalePrice | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 60 | RL | 65.0 | 8450 | Pave | NaN | Reg | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2008 | WD | Normal | 208500 |
1 | 2 | 20 | RL | 80.0 | 9600 | Pave | NaN | Reg | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2007 | WD | Normal | 181500 |
2 | 3 | 60 | RL | 68.0 | 11250 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 9 | 2008 | WD | Normal | 223500 |
3 rows × 81 columns
회귀 모델을 적용하기 전에 타깃 값의 분포도가 정규 분포인지 확인!!
sns.distplot(house_df['SalePrice'])
plt.show()
# 타깃 값의 분포가 정규 분포가 아니기 때문에 정규 분포 형태로 변환하기 위해 로그 변환을 적용
log_SalePrice = np.log1p(house_df['SalePrice'])
sns.distplot(log_SalePrice)
plt.show()
로그 변환을 했더니 정규 분포 형태로 결괏값이 분포한다 -> 사용하면 되겠군!!
# Null 값이 많은 피처 확인
isnull_series = house_df.isnull().sum()
isnull_series[isnull_series > 0].sort_values(ascending = False)
PoolQC 1453 MiscFeature 1406 Alley 1369 Fence 1179 FireplaceQu 690 LotFrontage 259 GarageType 81 GarageYrBlt 81 GarageFinish 81 GarageQual 81 GarageCond 81 BsmtExposure 38 BsmtFinType2 38 BsmtFinType1 37 BsmtCond 37 BsmtQual 37 MasVnrArea 8 MasVnrType 8 Electrical 1 dtype: int64
# Null 값이 많은 5개열, id열 제거, LotFrontage부터는 평균값으로 대체
original_SalePrice = house_df['SalePrice'] # 로그 변환할 것이기 때문에 기존 타깃 column 따로 저장
house_df['SalePrice'] = np.log1p(house_df['SalePrice'])
house_df.drop( ["Id", "PoolQC", "MiscFeature", "Alley", "Fence"], axis=1, inplace=True )
house_df.fillna(house_df.mean(), inplace=True)
# Null값이 있는 피처명과 타입을 추출
null_column_count = house_df.isnull().sum()[house_df.isnull().sum() > 0]
print('## Null 피처의 Type : \n', house_df.dtypes[null_column_count.index])
## Null 피처의 Type : MasVnrType object BsmtQual object BsmtCond object BsmtExposure object BsmtFinType1 object BsmtFinType2 object Electrical object FireplaceQu object GarageType object GarageFinish object GarageQual object GarageCond object dtype: object
이제 문자형 피처를 제외하고는 Null 값이 없다!!
# 문자형 피처 - 원핫 인코딩
house_df_ohe = pd.get_dummies(house_df) ; house_df_ohe
MSSubClass | LotFrontage | LotArea | OverallQual | OverallCond | YearBuilt | YearRemodAdd | MasVnrArea | BsmtFinSF1 | BsmtFinSF2 | ... | SaleType_ConLw | SaleType_New | SaleType_Oth | SaleType_WD | SaleCondition_Abnorml | SaleCondition_AdjLand | SaleCondition_Alloca | SaleCondition_Family | SaleCondition_Normal | SaleCondition_Partial | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 60 | 65.0 | 8450 | 7 | 5 | 2003 | 2003 | 196.0 | 706 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
1 | 20 | 80.0 | 9600 | 6 | 8 | 1976 | 1976 | 0.0 | 978 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
2 | 60 | 68.0 | 11250 | 7 | 5 | 2001 | 2002 | 162.0 | 486 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
3 | 70 | 60.0 | 9550 | 7 | 5 | 1915 | 1970 | 0.0 | 216 | 0 | ... | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
4 | 60 | 84.0 | 14260 | 8 | 5 | 2000 | 2000 | 350.0 | 655 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1455 | 60 | 62.0 | 7917 | 6 | 5 | 1999 | 2000 | 0.0 | 0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
1456 | 20 | 85.0 | 13175 | 6 | 6 | 1978 | 1988 | 119.0 | 790 | 163 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
1457 | 70 | 66.0 | 9042 | 7 | 9 | 1941 | 2006 | 0.0 | 275 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
1458 | 20 | 68.0 | 9717 | 5 | 6 | 1950 | 1996 | 0.0 | 49 | 1029 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
1459 | 20 | 75.0 | 9937 | 5 | 6 | 1965 | 1965 | 0.0 | 830 | 290 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
1460 rows × 276 columns
예측 평가를 RMSLE ( 실제 값과 예측값의 오류를 로그 변환한 뒤 RMSE를 적용 )를 이용하지만 이미 타깃 값을 로그 변환했기 때문에 예측값도 로그 변환된 값이 나올 것이고, 따라서 RMSE만 적용하면 자동으로 RMSLE가 측정된다.
# RMSE
# 단일 모델의 RMSE 값
def get_rmse(model):
pred = model.predict(X_test)
mse = mean_squared_error(y_test , pred)
rmse = np.sqrt(mse)
print(f'{model.__class__.__name__} 로그 변환된 RMSE: {np.round(rmse, 4)}')
return rmse
# 여러 모델의 RMSE 값 반환
def get_rmses(models):
rmses = []
for model in models:
rmse = get_rmse(model)
rmses.append(rmse)
return rmses
# 선형 회귀 모델 학습, 예측
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
X_features = house_df_ohe.drop('SalePrice',axis=1, inplace=False)
y_target = house_df_ohe['SalePrice']
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2, random_state=156)
# LinearRegression, Ridge, Lasso 학습 , 예측, 평가
lr_reg = LinearRegression()
ridge_reg = Ridge()
lasso_reg = Lasso()
lr_reg.fit(X_train, y_train)
ridge_reg.fit(X_train, y_train)
lasso_reg.fit(X_train, y_train)
models = [lr_reg, ridge_reg, lasso_reg]
rmses = get_rmses(models)
LinearRegression 로그 변환된 RMSE: 0.1322 Ridge 로그 변환된 RMSE: 0.1282 Lasso 로그 변환된 RMSE: 0.1763
Lasso모델의 경우 타 모델들보다 떨어지는 결과가 나왔고 최적 하이퍼 파라미터 튜닝이 필요해 보인다.
# 피처별 회귀 꼐수 시각화 -> 모델별로 어떠한 피처의 회귀 계수로 구성되는지 확인
def get_top_bottom_coef(model):
# coef_ 속성을 기반으로 Series 객체를 생성
coef = pd.Series(model.coef_, index = X_features.columns)
# + 상위 10개, - 상위 10개
coef_high = coef.sort_values(ascending=False).head(10)
coef_low = coef.sort_values(ascending=False).tail(10)
return coef_high, coef_low
# 시각화를 위한 함수
def visualize_coefficient(models, xlim=None):
fig, axs = plt.subplots(figsize=(24, 10), nrows = 1, ncols = 3)
fig.tight_layout()
# 입력 인자로 받은 list 객체인 models에서 차례로 model을 추출해 회귀 계수 시각화
for i, model in enumerate(models):
# 상위 10, 하위 10, concat
coef_high, coef_low = get_top_bottom_coef(model)
coef_concat = pd.concat([coef_high , coef_low])
# barchar로 표현, 한 화면에 표시하기 위해 tick label과 font 조정
axs[i].set_title(model.__class__.__name__ , size=25)
axs[i].tick_params(axis = "y", direction = "in", pad = -120) # tick label 위치
# font size
for label in (axs[i].get_xticklabels() + axs[i].get_yticklabels()):
label.set_fontsize(17)
sns.barplot(x=coef_concat.values, y=coef_concat.index , ax=axs[i])
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)
Lasso는 전체적으로 회귀 계수 값이 매우 작고, 예외적으로 YearBuilt의 회귀 계수만 너무 크다. -> 학습 데이터의 데이터 분할에 문제가 있어서 그런 것인지 확인 ( cross_val_score 확인 )
from sklearn.model_selection import cross_val_score
def get_avg_rmse_cv(models):
for model in models:
mse_scores = -1*cross_val_score(model, X_features, y_target, scoring="neg_mean_squared_error", cv=5)
rmse_scores = np.sqrt(mse_scores)
rmse_avg = np.mean(rmse_scores)
print(f"{model.__class__.__name__} CV RMSE 리스트: {np.round(rmse_scores,3)}")
print(f"{model.__class__.__name__} CV 평균 RMSE: {np.round(rmse_avg,3)}")
# 앞 예제에서 학습한 3가지 모델의 CV RMSE 값 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_avg_rmse_cv(models)
LinearRegression CV RMSE 리스트: [1.36000e-01 2.27823e+03 1.67000e-01 1.11000e-01 1.97000e-01] LinearRegression CV 평균 RMSE: 455.768 Ridge CV RMSE 리스트: [0.118 0.156 0.142 0.117 0.188] Ridge CV 평균 RMSE: 0.144 Lasso CV RMSE 리스트: [0.161 0.204 0.177 0.181 0.265] Lasso CV 평균 RMSE: 0.198
from sklearn.model_selection import cross_val_score
def get_avg_rmse_cv(models):
for model in models:
mse_scores = -1*cross_val_score(model, X_features, y_target, scoring="neg_mean_squared_error", cv=10)
rmse_scores = np.sqrt(mse_scores)
rmse_avg = np.mean(rmse_scores)
print(f"{model.__class__.__name__} CV RMSE 리스트: {np.round(rmse_scores,3)}")
print(f"{model.__class__.__name__} CV 평균 RMSE: {np.round(rmse_avg,3)}")
# 앞 예제에서 학습한 3가지 모델의 CV RMSE 값 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_avg_rmse_cv(models)
LinearRegression CV RMSE 리스트: [0.133 0.123 0.122 0.193 0.145 0.173 0.123 0.098 0.246 0.129] LinearRegression CV 평균 RMSE: 0.148 Ridge CV RMSE 리스트: [0.121 0.105 0.12 0.179 0.149 0.124 0.126 0.106 0.234 0.132] Ridge CV 평균 RMSE: 0.14 Lasso CV RMSE 리스트: [0.167 0.155 0.175 0.23 0.197 0.152 0.187 0.172 0.339 0.17 ] Lasso CV 평균 RMSE: 0.194
fold를 5로 할때와 10으로 할 때의 LR의 평균 RMSE 차이가 엄청나게 크다 -> 랜덤으로 선택되다 보니 편차가 큰 것으로 보임 !!
5-fold로 학습해도 여전히 Lasso의 점수가 성능이 떨어진다. -> 하이퍼 파라미터를 변화시키면서 최적 값을 도출!
# gridsearch를 해서 최적의 파라미터를 찾아주는 함수
from sklearn.model_selection import GridSearchCV
def print_best_params(model, params):
grid_model = GridSearchCV(model, param_grid = params,
scoring='neg_mean_squared_error', cv=5)
grid_model.fit(X_features, y_target)
rmse = np.sqrt(-1* grid_model.best_score_)
print(f'{model.__class__.__name__} GridSearchCV 최적 평균 RMSE 값 : {np.round(rmse, 4)}, 최적 alpha:{grid_model.best_params_}')
return grid_model.best_estimator_
ridge_params = { 'alpha':[0.05, 0.1, 1, 5, 8, 10, 12, 15, 20] }
lasso_params = { 'alpha':[0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1,5, 10] }
best_ridge = print_best_params(ridge_reg, ridge_params)
best_lasso = print_best_params(lasso_reg, lasso_params)
Ridge GridSearchCV 최적 평균 RMSE 값 : 0.1418, 최적 alpha:{'alpha': 12} Lasso GridSearchCV 최적 평균 RMSE 값 : 0.142, 최적 alpha:{'alpha': 0.001}
# 앞의 최적화 alpha 값을 사용해서 학습 데이터로 학습, 테스트 데이터로 예측 및 평가
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge(alpha=12)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)
# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)
# 모든 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)
LinearRegression 로그 변환된 RMSE: 0.1322 Ridge 로그 변환된 RMSE: 0.1246 Lasso 로그 변환된 RMSE: 0.1204
alpha 값을 최적화 하고 나니 테스트 데이터의 예측 성능이 더 좋아졌다. ( 모델별로 회귀 계수도 많이 달라졌다. ) 이번에는 라쏘 모델의 성능이 가장 좋지만 릿지에 비해 동일한 피처라도 회귀 계수의 값이 상당히 작은 것을 확인할 수 있다.
피처 데이터 세트의 데이터 분포도
피처 데이터 세트의 경우 지나치게 왜곡된 피처가 존재할 경우 회귀 예측 성능을 저하시킬 수 있다. -> 모든 숫자형 피처의 데이터 분포도를 확인해 분포도가 어느 정도로 왜곡됐는지 확인
stat 모듈의 skew 함수를 이용해 왜곡 정도를 확인. 이 때 skew를 적용하는 숫자형 피처에서 원핫 인코딩 된 카테고리 숫자형 피처는 제외해야 한다!! 카테고리 피처는 코드성 피처이므로 인코딩시 당연히 왜곡될 가능성이 높기 때문이다.
from scipy.stats import skew
# 숫자형 피처의 칼럼 index 객체 추출
features_index = house_df.dtypes[house_df.dtypes != 'object'].index
# house_df에 칼럼 index를 [ ] 로 입력하면 해당하는 칼럼 데이터 세트 반환
skew_features = house_df[features_index].apply(lambda x : skew(x), axis=0)
# 왜곡 정도가 1 이상인 칼럼만 추출
skew_features_top = skew_features[skew_features > 1]
print(skew_features_top.sort_values(ascending=False))
MiscVal 24.451640 PoolArea 14.813135 LotArea 12.195142 3SsnPorch 10.293752 LowQualFinSF 9.002080 KitchenAbvGr 4.483784 BsmtFinSF2 4.250888 ScreenPorch 4.117977 BsmtHalfBath 4.099186 EnclosedPorch 3.086696 MasVnrArea 2.673661 LotFrontage 2.382499 OpenPorchSF 2.361912 BsmtFinSF1 1.683771 WoodDeckSF 1.539792 TotalBsmtSF 1.522688 MSSubClass 1.406210 1stFlrSF 1.375342 GrLivArea 1.365156 dtype: float64
# 추출된 왜곡 정도가 높은 피처를 로그 변환
house_df[skew_features_top.index] = np.log1p(house_df[skew_features_top.index])
로그 변환 후 이 피처들의 왜곡 정도를 다시 확인해보면 여전히 높은 왜곡 정도를 가진 피처가 있지만 더 이상 로그 변환을 하더라도 개선하기 어려움. house_df의 일부 피처들을 로그 변환했으므로 다시 원-핫 인코딩을 적용한 house_df_ohe 생성
house_df_ohe = pd.get_dummies(house_df)
y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop('SalePrice', axis = 1, inplace = False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size = 0.2, random_state = 156)
# 피처를 로그 변환한 후 다시 최적 하이퍼 파라미터와 RMSE 출력
ridge_params = { 'alpha':[0.05, 0.1, 1, 5, 8, 10, 12, 15, 20] }
lasso_params = { 'alpha':[0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1,5, 10] }
best_ridge = print_best_params(ridge_reg, ridge_params)
best_lasso = print_best_params(lasso_reg, lasso_params)
Ridge GridSearchCV 최적 평균 RMSE 값 : 0.1277, 최적 alpha:{'alpha': 12} Lasso GridSearchCV 최적 평균 RMSE 값 : 0.1253, 최적 alpha:{'alpha': 0.001}
5-fold 교차 검증의 평균 RMSE 값이 두 모델 모두 향상된 모습.
- 타깃 데이터 로그 변환
- 피처 데이터 skew로 1 이상인 값을 로그 변환한 후 원-핫 인코딩으로 피처 만들고 최적의 alpha값을 찾아 해당 파라미터 적용
# 앞의 최적화 alpha 값을 사용해서 학습 데이터로 학습, 테스트 데이터로 예측 및 평가
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge(alpha=12)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)
# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)
# 모든 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)
LinearRegression 로그 변환된 RMSE: 0.1288 Ridge 로그 변환된 RMSE: 0.1232 Lasso 로그 변환된 RMSE: 0.1195
일반적으로 생각할 수 있는 주거 공간 크기가 회귀 계수가 가장 높은 상관관계를 가질 것이라는 결과가 도출되었다.
이상치 데이터
특히 회귀 계수가 높은 피처, 즉 예측에 많은 영향을 미치는 중요 피처의 이상치 데이터 처리가 중요하다.
# 회귀 계수가 가장 높은 GrLivArea 피처의 데이터 분포를 확인 ( 원래 데이터 )
plt.scatter(house_df_org["GrLivArea"] , house_df_org["SalePrice"], c="darkorange")
plt.ylabel('SalePrice')
plt.xlabel('GrLivArea')
plt.show()
오른쪽에 보이는 2개의 이상치 데이터가 눈에 띈다. 따라서 GrLivArea가 4000 이상임에도 가격이 500,000이하인 데이터는 모두 이상치로 간주하고 삭제
# GrLivArea, SalePrice 모두 로그 변환 하였으므로 반영한 조건 생성
cond1 = house_df_ohe['GrLivArea'] > np.log1p(4000)
cond2 = house_df_ohe['SalePrice'] < np.log1p(500000)
outlier_index = house_df_ohe[cond1 & cond2].index
print('이상치 레코드 index :', outlier_index.values)
print('이상치 삭제 전 house_df_ohe shape:', house_df_ohe.shape)
# DataFrame의 인덱스를 이용해 이상치 레코드 삭제
house_df_ohe.drop(outlier_index , axis=0, inplace=True)
print('이상치 삭제 후 house_df_ohe shape:', house_df_ohe.shape)
이상치 레코드 index : [ 523 1298] 이상치 삭제 전 house_df_ohe shape: (1460, 276) 이상치 삭제 후 house_df_ohe shape: (1458, 276)
y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop('SalePrice', axis = 1, inplace = False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size = 0.2, random_state = 156)
# 피처를 로그 변환한 후 다시 최적 하이퍼 파라미터와 RMSE 출력
ridge_params = { 'alpha':[0.05, 0.1, 1, 5, 8, 10, 12, 15, 20] }
lasso_params = { 'alpha':[0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1,5, 10] }
best_ridge = print_best_params(ridge_reg, ridge_params)
best_lasso = print_best_params(lasso_reg, lasso_params)
Ridge GridSearchCV 최적 평균 RMSE 값 : 0.113, 최적 alpha:{'alpha': 8} Lasso GridSearchCV 최적 평균 RMSE 값 : 0.1123, 최적 alpha:{'alpha': 0.001}
단 2개의 이상치 데이터만 제거 했는데도 예측 수치가 매우 크게 향상된 모습을 볼 수 있다. 또한 릿지 모델의 최적 alpha의 값이 12 -> 8로 변했다. GrLivArea 속성이 회귀 모델에서 차지하는 영향도가 크기에 이상치를 개선하는 것이 성능 개선에 큰 의미를 지녔다.
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge(alpha=12)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)
# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)
LinearRegression 로그 변환된 RMSE: 0.1283 Ridge 로그 변환된 RMSE: 0.1031 Lasso 로그 변환된 RMSE: 0.1002
[0.12834028292484415, 0.10310568457369433, 0.1001723101982164]
회귀 트리 모델 학습 / 예측 / 평가
밑의 경우에는 하이퍼 파라미터는 고정!
# XGBoost
from xgboost import XGBRegressor
xgb_params = {'n_estimators':[1000]}
xgb_reg = XGBRegressor(learning_rate=0.05, colsample_bytree=0.5, subsample=0.8)
best_xgb = print_best_params(xgb_reg, xgb_params)
XGBRegressor GridSearchCV 최적 평균 RMSE 값 : 0.1163, 최적 alpha:{'n_estimators': 1000}
# LGBM
from lightgbm import LGBMRegressor
lgbm_params = {'n_estimators':[1000]}
lgbm_reg = LGBMRegressor(learning_rate=0.05, num_leaves=4,
subsample=0.6, colsample_bytree=0.4, reg_lambda=10, n_jobs=-1)
best_lgbm = print_best_params(lgbm_reg, lgbm_params)
LGBMRegressor GridSearchCV 최적 평균 RMSE 값 : 0.1175, 최적 alpha:{'n_estimators': 1000}
# 모델의 중요도 상위 20개의 피처명과 그때의 중요도값을 Series로 반환.
def get_top_features(model):
ftr_importances_values = model.feature_importances_
ftr_importances = pd.Series(ftr_importances_values, index=X_features.columns )
ftr_top20 = ftr_importances.sort_values(ascending=False)[:20]
return ftr_top20
def visualize_ftr_importances(models):
# 2개 회귀 모델의 시각화를 위해 2개의 컬럼을 가지는 subplot 생성
fig, axs = plt.subplots(figsize=(24,10),nrows=1, ncols=2)
fig.tight_layout()
# 입력인자로 받은 list객체인 models에서 차례로 model을 추출하여 피처 중요도 시각화.
for i_num, model in enumerate(models):
# 중요도 상위 20개의 피처명과 그때의 중요도값 추출
ftr_top20 = get_top_features(model)
axs[i_num].set_title(model.__class__.__name__+' Feature Importances', size=25)
#font 크기 조정.
for label in (axs[i_num].get_xticklabels() + axs[i_num].get_yticklabels()):
label.set_fontsize(22)
sns.barplot(x=ftr_top20.values, y=ftr_top20.index , ax=axs[i_num])
# 앞 예제에서 print_best_params( )가 반환한 GridSearchCV로 최적화된 모델의 피처 중요도 시각화
models = [best_xgb, best_lgbm]
visualize_ftr_importances(models)
회귀 모델의 예측 결과 혼합을 통한 예측
개별 회귀 모델의 예측 결과값을 혼합해 이를 기반으로 최종 회귀 값을 예측. ( 서브미션 앙상블로 생각하면 될듯 )
def get_rmse_pred(preds):
for key in preds.keys():
pred_value = preds[key]
mse = mean_squared_error(y_test , pred_value)
rmse = np.sqrt(mse)
print('{0} 모델의 RMSE: {1}'.format(key, rmse))
# 개별 모델의 학습
ridge_reg = Ridge(alpha=8)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)
# 개별 모델 예측
ridge_pred = ridge_reg.predict(X_test)
lasso_pred = lasso_reg.predict(X_test)
# 개별 모델 예측값 혼합으로 최종 예측값 도출
pred = 0.4 * ridge_pred + 0.6 * lasso_pred
preds = {'최종 혼합': pred,
'Ridge': ridge_pred,
'Lasso': lasso_pred}
#최종 혼합 모델, 개별모델의 RMSE 값 출력
get_rmse_pred(preds)
최종 혼합 모델의 RMSE: 0.10008401393966342 Ridge 모델의 RMSE: 0.10359889117940774 Lasso 모델의 RMSE: 0.1001723101982164
# xgb, lgbm
xgb_reg = XGBRegressor(n_estimators=1000, learning_rate=0.05,
colsample_bytree=0.5, subsample=0.8)
lgbm_reg = LGBMRegressor(n_estimators=1000, learning_rate=0.05, num_leaves=4,
subsample=0.6, colsample_bytree=0.4, reg_lambda=10, n_jobs=-1)
xgb_reg.fit(X_train, y_train)
lgbm_reg.fit(X_train, y_train)
xgb_pred = xgb_reg.predict(X_test)
lgbm_pred = lgbm_reg.predict(X_test)
pred = 0.5 * xgb_pred + 0.5 * lgbm_pred
preds = {'최종 혼합': pred,
'XGBM': xgb_pred,
'LGBM': lgbm_pred}
get_rmse_pred(preds)
최종 혼합 모델의 RMSE: 0.10267340377090536 XGBM 모델의 RMSE: 0.107286147196175 LGBM 모델의 RMSE: 0.10392491443937421
스태킹 앙상블 모델을 통한 회귀 예측
분류에서 사용한 스태킹 모델을 회귀에도 적용할 수 있다. 스태킹 모델에는 두 종류의 모델이 필요한데 첫 번째는 개별적인 기반 모델, 두 번째는 이 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 최종 메타 모델이다.
from sklearn.model_selection import KFold
from sklearn.metrics import mean_absolute_error
# 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터를 생성하기 위한 함수.
def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds ):
# 지정된 n_folds값으로 KFold 생성.
kf = KFold(n_splits=n_folds, shuffle=False)
# 추후에 메타 모델이 사용할 학습 데이터 반환을 위한 넘파이 배열 초기화
train_fold_pred = np.zeros((X_train_n.shape[0] ,1 ))
test_pred = np.zeros((X_test_n.shape[0],n_folds))
print(model.__class__.__name__ , ' model 시작 ')
for folder_counter , (train_index, valid_index) in enumerate(kf.split(X_train_n)):
#입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 셋 추출
print('\t 폴드 세트: ',folder_counter,' 시작 ')
X_tr = X_train_n[train_index]
y_tr = y_train_n[train_index]
X_te = X_train_n[valid_index]
#폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수행.
model.fit(X_tr , y_tr)
#폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델 예측 후 데이터 저장.
train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1,1)
#입력된 원본 테스트 데이터를 폴드 세트내 학습된 기반 모델에서 예측 후 데이터 저장.
test_pred[:, folder_counter] = model.predict(X_test_n)
# 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성
test_pred_mean = np.mean(test_pred, axis=1).reshape(-1,1)
#train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
return train_fold_pred , test_pred_mean
get_stacking_base_datasets( )는 인자로 개별 기반 모델, 학습 데이터, 테스트용 피처 데이터를 입력 받는다. 함수 내에서는 개별 모델이 K-fold 세트로 설정된 폴드 세트 내부에서 원본의 학습 데이터를 다시 추출해 학습과 예측을 수행한 뒤 그 결과를 저장한다. 저장된 예측 데이터는 추후에 메타 모델의 학습 피처 데이터 세트로 이용된다. 또한 함수 내에서 폴드 세트 내부 학습 데이터로 학습된 개별 모델이 인자로 입력된 원본 테스트 데이터를 예측한 뒤, 예측 결과를 평균해 테스트 데이터로 생성한다.
# 사용할 개별 모델 : 릿지, 라쏘, XGBoost, LGBM
# get_stacking_base_datasets( )은 넘파이 ndarray를 인자로 사용하므로 DataFrame을 넘파이로 변환.
X_train_n = X_train.values
X_test_n = X_test.values
y_train_n = y_train.values
# 각 개별 기반(Base)모델이 생성한 학습용/테스트용 데이터 반환.
ridge_train, ridge_test = get_stacking_base_datasets(ridge_reg, X_train_n, y_train_n, X_test_n, 5)
lasso_train, lasso_test = get_stacking_base_datasets(lasso_reg, X_train_n, y_train_n, X_test_n, 5)
xgb_train, xgb_test = get_stacking_base_datasets(xgb_reg, X_train_n, y_train_n, X_test_n, 5)
lgbm_train, lgbm_test = get_stacking_base_datasets(lgbm_reg, X_train_n, y_train_n, X_test_n, 5)
Ridge model 시작 폴드 세트: 0 시작 폴드 세트: 1 시작 폴드 세트: 2 시작 폴드 세트: 3 시작 폴드 세트: 4 시작 Lasso model 시작 폴드 세트: 0 시작 폴드 세트: 1 시작 폴드 세트: 2 시작 폴드 세트: 3 시작 폴드 세트: 4 시작 XGBRegressor model 시작 폴드 세트: 0 시작 폴드 세트: 1 시작 폴드 세트: 2 시작 폴드 세트: 3 시작 폴드 세트: 4 시작 LGBMRegressor model 시작 폴드 세트: 0 시작 폴드 세트: 1 시작 폴드 세트: 2 시작 폴드 세트: 3 시작 폴드 세트: 4 시작
# 개별 모델이 반환한 학습 및 테스트용 데이터 세트를 Stacking 형태로 결합.
Stack_final_X_train = np.concatenate((ridge_train, lasso_train,
xgb_train, lgbm_train), axis=1)
Stack_final_X_test = np.concatenate((ridge_test, lasso_test,
xgb_test, lgbm_test), axis=1)
# 최종 메타 모델은 라쏘 모델을 적용.
meta_model_lasso = Lasso(alpha=0.0005)
#기반 모델의 예측값을 기반으로 새롭게 만들어진 학습 및 테스트용 데이터로 예측하고 RMSE 측정.
meta_model_lasso.fit(Stack_final_X_train, y_train)
final = meta_model_lasso.predict(Stack_final_X_test)
mse = mean_squared_error(y_test , final)
rmse = np.sqrt(mse)
print('스태킹 회귀 모델의 최종 RMSE 값은:', rmse)
스태킹 회귀 모델의 최종 RMSE 값은: 0.09751366723899443
최종 모델은 RMSE 값이 매우 낮아진 모습!! , 스태킹은 분류에서도 회귀에서도 효과적으로 사용된다
# 업로드용 창 맞추기
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:90% !important; }</style>"))
'머신러닝(Machine Learning)' 카테고리의 다른 글
[파이썬 머신러닝 완벽 가이드] 6장. 차원 축소 (0) | 2022.01.30 |
---|---|
[파이썬 머신러닝 완벽 가이드] 5장. 회귀 - 1 (0) | 2022.01.23 |
[파이썬 머신러닝 완벽 가이드] 4장. 분류 - 2 (0) | 2022.01.22 |
댓글