SELECT LEFT('MySQL is database management system', 5); >> MySQL SELECT RIGHT('MySQL is database management system', 6); >> system
LOWER(), UPPER()
- LOWER(): 문자열의 문자를 모두 소문자로 변경
- UPPER(): 문자열의 문자를 모두 대문자로 변경
SELECT LOWER('MySQL is database management system'); >> mysql is database management system SELECT UPPER('MySQL is database management system'); >> MYSQL IS DATABASE MANAGEMENT SYSTEM
REPLACE()
- 문자열에서 특정 문자열을 대체 문자열로 교체
SELECT REPLACE('MSSQL', 'MS', 'My'); >> MySQL
TRIM()
- 문자열의 앞이나 뒤, 또는 양쪽 모두에 있는 특정 문자를 제거
- TRIM() 함수에서 사용할 수 있는 지정자
BOTH: 전달받은 문자열의 양 끝에 존재하는 특정 문자를 제거(기본 설정)
LEADING: 전달받은 문자열 앞에 존재하는 특정 문자를 제거
TRAILING: 전달받은 문자열 뒤에 존재하는 특정 문자를 제거
- 제거할 문자를 명시하지 않으면, 자동으로 공백을 제거
SELECT TRIM(' MySQL '), TRIM(LEADING '#' FROM '###MySQL##'), TRIM(TRAILING '#' FROM '###MySQL##'); >> MySQL, MySQL##, ###MySQL
FORMAT()
- 숫자 타입의 데이터를 세 자리마다 쉼표(,)를 사용하는 '#,###,###.##' 형식으로 변환
SELECT * FROM city WHERE Population > 7000000 AND Population < 8000000;
BETWEEN
데이터가 숫자로 구성되어 있어 연속적인 범위를 지정할 때 BETWEEN...AND 사용 가능(이상, 이하의 결과를 추출)
SELECT * FROM city WHERE Population BETWEEN 7000000 AND 8000000;
# 동일한 결과(AND 연산자 사용) SELECT * FROM city WHERE Poulation >= 7000000 AND Population <= 8000000;
# 위 검색내용과 반대되는 조건문 SELECT * FROM city WHERE Population NOT BETWEEN 7000000 AND 8000000;
IN
이산적인 값의 조건에서는 IN()사용 가능(IN안에 들어가는 값에 맞는 결과 추출)
SELECT * FROM city WHERE Name IN('Seoul', 'New York', 'Tokyo');
# 마찬가지로 NOT 이용 가능(반대되는 조건문)
LIKE
- 문자열의 내용 검색하기 위해 LIKE 연산자 사용(부분적으로 일치하는 데이터를 찾을때 사용)
- _: 글자숫자를 정해줌(EX 컬럼명 LIKE '홍_동'), 한 글자와 매치
- %: 글자숫자를 정해주지않음(EX 컬럼명 LIKE '홍%'), 모든 글자와 매치
# A로 시작하는 문자를 찾기 SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE 'A%';
# A로 끝나는 문자 찾기 SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE '%A';
# A를 포함하는 문자 찾기 SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE '%A%';
# A로 시작하는 두 글자 문자 찾기 SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE 'A_';
# 첫 번째 문자가 'A'가 아닌 모든 문자열 찾기 SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE '[^A]';
# 첫 번째 문자가 'A' 또는 'B' 또는 'C'인 문자열 찾기 SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE '[ABC]'; SELECT 컬럼명 FROM 테이블 WHERE 컬럼명 LIKE '[A-C]';
Sub Query(서브 쿼리)
- 쿼리문 안에 또 쿼리문이 들어 있는 것
- 서브 쿼리의 결과가 둘 이상이 되면 에러 발생
- 서브쿼리를 쓰는 이유는 알려지지 않은 기준을 이용한 검색을 하기 위해서
SELECT * FROM city WHERE CountryCode = ( SELECT CountryCode FROM city WHERE Name = 'Seoul' );
서브 쿼리만 실행하면 city 테이블에서 Name이 'Seoul'인 CountryCode가 출력되고('KOR') 메인 쿼리와 매칭시켜 결과 출력
# 서브 쿼리의 결과가 둘 이상이 되면 에러 발생 SELECT * FROM city WHERE CountryCode = ( SELECT CountryCode FROM city WHERE Name IN('Seoul', 'Kabul') );
ANY(서브쿼리 안의 값이 여러개일때)
- 서브쿼리의 여러 개의 결과 중 한 가지만 만족해도 가능(하나의 조건만 만족하면 True)
- SOME은 ANY와 동일한 의미로 사용
SELECT * FROM city WHERE Population > ANY( SELECT Population FROM city WHERE District = 'New York' ); # > ANY : 최소값보다 크면 >= ANY : 최소값보다 크거나 같으면 < ANY : 최대값보다 작으면 <= ANY : 최대값보다 작거나 같으면 = ANY : IN과 같은 기능 != ANY : NOT IN과 같은 기능
ALL(서브쿼리 안의 값이 여러개일때)
- 서브쿼리의 여러 개의 결과를 모두 만족시켜야 함(모든 값이 조건 만족하면 True)
SELECT * FROM city WHERE Population > ALL( SELECT Population FROM city WHERE District = 'New York' ); # > ALL : 최대값보다 크면 >= ALL : 최대값보다 크거나 같으면 < ALL : 최소값보다 작으면 <= ALL : 최소값보다 작거나 같으면 = ALL : 서브 쿼리의 결과가 1건이면 괜찮지만 여러 건이면 오류가 발생 != ALL : 서브 쿼리의 결과가 1건이면 괜찮지만 여러 건이면 오류가 발생
ORDER BY
- 결과가 출력되는 순서를 조절하는 구문
- default는 ASC(오름차순)으로 생략 가능
- 내림차순 정렬(열 이름 뒤에 DESC 적어줄 것)
SELECT * FROM city ORDER BY Population DESC;
- 혼합해 사용하는 구문도 가능
SELECT * FROM city ORDER BY CountryCode ASC, Population DESC; # CountryCode 컬럼을 오름차순으로 정렬 후, Population 컬럼을 내림차순 정렬
DISTINCT
- 중복된 것은 1개씩만 보여주면서 출력
- 테이블의 크기가 클수록 효율적
SELECT DISTINCT CountryCode FROM city;
LIMIT
- 출력 개수를 제한
- 상위의 N개만 출력하는 'LIMIT N' 구문
- 서버의 처리량을 많이 사용해 서버의 전반적인 성능을 나쁘게 하는 악성 쿼리문 개선할 때 사용
- Workbench에서 Limit을 조절하는 옵션이 있음
SELECT * FROM city ORDER BY Population DESC LIMIT 10;
GROUP BY
- 그룹으로 묶어주는 역할(효율적인 데이터 그룹화)
- 집계 함수를 함께 사용
AVG(): 평균
MIN(): 최소값
MAX(): 최대값
COUNT(): 행의 개수
COUNT(DISTINCT): 중복 제외된 행의 개수
STDEV(): 표준편차
VARIANCE(): 분산
- 읽기 좋게 하기 위해 별칭 사용: AS
SELECT CountryCode, AVG(Population) AS 'Average' FROM city GROUP BY CountryCode;
Question: 도시는 몇개인가?, 도시들의 평균 인구수는?
SELECT COUNT(*) FROM city;
SELECT AVG(Population) FROM city;
HAVING
- WHERE과 비슷한 개념으로 조건 제한
- 집계 함수에 대해서 조건 제한하는 편리한 개념
- HAVING절은 반드시 GROUP BY절 다음에 나와야 함
SELECT CountryCode, MAX(Population) FROM city GROUP BY CountryCode HAVING MAX(Population) > 8000000;
ROLLUP
- 총합 또는 중간합계가 필요할 경우 사용
- GROUP BY절과 함께 WITH ROLLUP문 사용
SELECT CountryCode, Name, SUM(Population) FROM city GROUP BY CountryCode, Name WITH ROLLUP;
JOIN
- 데이터베이스 내의 여러 테이블을 하나의 테이블이나 결과 집합으로 결합
- python에서 데이터프레임을 결합하는 merge와 비슷
SELECT * FROM city JOIN country ON city.CountryCode = country.Code;
Question: city, country, countrylanguage 테이블 3개를 JOIN 하기
SELECT * FROM city JOIN country ON city.CountryCode = country.Code JOIN countrylanguage ON city.CountryCode = countrylanguage.CountryCode;
○ 선형 회귀 모델은 일반적으로 피처와 타겟값 간에 선형의 관계가 있다고 가정하고 이러한 최적의 선형 함수를 찾아내 결과값을 예측함
○ 선형 회귀 모델은 피처값과 타겟값의 분포가 정규분포 형태를 선호
- 피처값과 타겟값의 분포가 왜곡(skew)된 분포일 경우 예측 성능에 부정적인 영향을 미칠 수 있음
→ 선형 회귀 모델을 적용하기 전에 먼저 데이터에 대한 스케일링/정규화 작업을 수행하는 것이 일반적이다. 하지만 이러한 스케일링/정규화 작업을 선행한다고 해서 무조건 예측 성능이 향상되는 것은 아니다.
<'피처값 변환 - 로그변환' 부가적인 설명>
로그변환은 매우 유용한 변환이며, 원래 값에 log 함수를 적용하면 보다 정규분포에 가까운 형태로 값이 분포된다.
실제로 스케일링, 다항 특성 변환 방법보다 로그변환이 훨씬 많이 사용되는 변환 방법이다. 왜냐하면 스케일링 방법의 경우 예측 성능 향상을 크게 기대하기 어려운 경우가 많으며, 다항 특성 변환 방법의 경우 피처의 개수가 매우 많을 경우에는 다항 변환으로 생성되는 피처의 개수가 기하급수로 늘어나서 과적합의 이슈가 발생할 수 있기 때문이다.
from sklearn.preprocessing import StandardScaler, MinMaxScaler, PolynomialFeatures
# 보스턴 주택가격 타겟 데이터 세트는 이미 정규분포화 되어 있음
bostonDF['PRICE'].hist()
로그변환은 아래 코드에서 np.log()가 아니라 np.log1p()를 이용한다. 일반적으로 log() 함수를 적용하면 언더 플로우가 발생하기 쉬워서 1+log() 함수를 적용하는데 이를 구현한 것이 np.log1p()이다.
# method는 표준정규분포 변환(Standard), 최대값/최소값 정규화(MinMax), 로그변환(Log) 결정
# p_degree는 다항식 특성을 추가할 때 적용. p_degree는 2 이상 부여하지 않음
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, includ_bias=False).fit_transform(scaled_data)
return scaled_data
Ridge 클래스의 alpha 값을 변화시키면서 피처 데이터 세트를 여러 가지 방법으로 변환한 데이터 세트를 입력받을 경우에 rmse값이 어떻게 변하는지 살펴보자. (앞에서 생성한 get_linear_reg_eval() 함수 이용)
피처 데이터의 변환 방법은 모두 5가지로 아래와 같다.
(None, None) : 아무런 변환을 하지 않은 원본 데이터
('Standard', None) : 표준정규분포
('Standard', 2) : 표준정규분포를 다시 2차 다항식 변환
('MinMax', None) : 최소값/최대값 정규화
('MinMax', 2) : 최소값/최대값 정규화를 다시 2차 다항식 변환
('Log', None) : 로그변환
# 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_features_scaled = get_scaled_data(method=scale_method[0], p_degree=scale_method[1], input_data=X_features)
print('\n## 변환 유형:{0}, Polylnomial Degree:{1}'.format(scale_method[0], scale_method[1]))
get_linear_reg_eval('Ridge',params=alphas, X_features_n=X_features_scaled, y_target_n=y_target,
verbose=False, return_coeff=False)
## 변환 유형:None, Polylnomial 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, Polylnomial 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, Polylnomial 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, Polylnomial 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, Polylnomial 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, Polylnomial 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
표준정규분포와 최소값/최대값 정규화로 피처 데이터 세트를 변경해도 성능상의 개선이 별로 없음
표준정규분포로 일차 변환 후 2차 다항식 변환을 했을 때 alpha=100에서 4.634로 성능이 개선
최소값/최대값 정규화로 일차 변환 후 2차 다항식 변환을 했을 때 alpha=1에서 4.323으로 성능이 개선
하지만 다항식 변환은 피처의 개수가 많을 경우 적용하기 힘들고 과적합의 이슈가 발생할 수 있음
로그변환으로 피처 데이터 세트를 변경했을 때 alpha가 0.1, 1, 10인 경우에 모두 좋은 성능 향상이 있음
→ 일반적으로 선형 회귀를 적용하려는 데이터 세트에 데이터 값의 분포가 왜곡(skew)되어 있을 경우에 로그변환을 적용하는 것이 좋은 결과를 기대할 수 있고, 다항 특성 변환 방법은 예측에 대한 분산이 높고(alpha값에 따른 다항 특성 변환을 적용한 결과들을 보면 예측에 대한 분산이 높음), 과적합의 우려가 있어 최근에 많이 쓰이지 않음
다항회귀에서 degree1의 경우 지나치게 예측 곡선을 단순화해 과소적합 모델이 만들어졌고, degree15의 경우 지나치게 모든 학습 데이터에 적합한 회귀식을 만들기 위해서 다항식이 복잡해지고 회귀계수가 매우 크게 설정되면서 테스트 데이터 세트에 대해서 형편없는 예측 성능을 보이는 과적합 모델이 만들어졌다.
→ 회귀모델은 적절히 데이터에 적합하면서도 회귀계수가 기하급수적으로 커지는 것을 제어해야 함
이전까지 선형 모델의 비용함수는 RSS를 최소화하는, 즉 실제값과 예측값의 차이를 최소화 하는 것만 고려해 학습 데이터에 지나치게 맞춰지고 회귀계수가 쉽게 커져 과적합이 발생하는 경향이 있음
→ 위의 문제를 해결하기 위해 비용함수는 RSS 최소화 방법과 과적합을 방지하기 위해 회귀계수 값이 커지지 않도록 하는 방법이 서로 균형을 이루어야 함(학습 데이터의 잔차 오류와 회귀계수의 값이 함께 최소화하는 것을 목표로 함)
alpha는 학습 데이터 적합 정도와 회귀계수 값의 크기를 제어하는 파라미터로, 적절하게 설정해야한다.
○ alpha가 0(또는 매우 작은 값)이라면 Min(RSS(W) + 0)이 되면서 기존과 동일하게 RSS를 최소화하는 것만 고려하는 것과 다를게 없고
○ alpha가 무한대(또는 매우 큰 값)이라면 RSS(W)에 비해 alpha * llWll22 값이 너무 커지게 되므로 W를 0(또는 매우 작은 값)으로 만들어야 비용함수 목표를 달성할 수 있음
즉, alpha를 작게 하면 회귀계수 W의 값이 커져도 어느 정도 상쇄가 가능하므로 학습 데이터 적합을 더 개선할 수 있고, alpha를 크게 하면 회귀계수 W의 값을 작게 해 과적합을 개선할 수 있음
이처럼 비용함수에 alpha 값으로 페널티를 부여해 회귀계수 값의 크기를 감소시켜 과적합을 개선하는 방식을 규제(Regularization)라고 함
릿지 회귀(L2 Norm)
W의 제곱에 대해 페널티를 부여하는 방식. 회귀계수의 크기를 감소시키는 규제 모델
비용함수는
이고, 이 식을 최소화하는 W를 찾는 것이 목표이다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_boston
%matplotlib inline
boston = load_boston()
bostonDF = pd.DataFrame(boston.data, columns=boston.feature_names)
bostonDF['PRICE'] = boston.target
y_target = bostonDF['PRICE']
X_features = 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)
mse_scores = -cross_val_score(ridge, X_features, y_target, scoring='neg_mean_squared_error', cv=5)
rmse_scores = np.sqrt(mse_scores)
avg_rmse = np.mean(rmse_scores)
print('5fold의 개별 mse scores:', np.round(mse_scores, 3))
print('5fold의 개별 rmse scores:', np.round(rmse_scores, 3))
print('5fold의 평균 rmse:{0:.3f}'.format(avg_rmse))
alpha 값에 따른 rmse와 회귀계수를 살펴보자
alphas = [0, 0.1, 1, 10, 100]
for alpha in alphas:
ridge = Ridge(alpha=alpha)
# cross_val_score를 이용해 5 fold의 평균 rmse 계산
mse_scores = -cross_val_score(ridge, X_features, y_target, scoring='neg_mean_squared_error', cv=5)
avg_rmse = np.mean(np.sqrt(mse_scores))
print('alpha {0} 일 때 5 folds의 평균 rmse : {1:.3f}'.format(alpha, avg_rmse))
# 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_features, y_target)
# alpha에 따른 피처별로 회귀계수를 Series로 변환하고 이를 DataFrame의 컬럼으로 추가
coeff = pd.Series(ridge.coef_, index=X_features.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])
plt.show()
W의 절대값에 대해 페널티를 부여하는 방식. 불필요한 회귀계수를 급격하게 감소시켜 0으로 만드는 규제 모델
→ 이러한 측면에서 적절한 피처만 회귀에 포함시키는 피처 선택의 특성을 가지고 있음
비용함수는
이고, 이 식을 최소화하는 W를 찾는 것이 목표이다.
alpha 값에 따른 rmse와 회귀계수를 살펴보자. (alpha 값을 변화시키면서 결과를 출력하는 별도의 함수를 만들것)
from sklearn.linear_model import Lasso, ElasticNet
# alpha 값에 따른 회귀모델의 폴드 평균 rmse를 출력, 회귀계수 값들을 DataFrame으로 반환
def get_linear_reg_eval(model_name, params=None, X_features_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는 0.7로 고정시킴
mse_scores = -cross_val_score(model, X_features_n, y_target_n, scoring='neg_mean_squared_error', cv=5)
avg_rmse = np.mean(np.sqrt(mse_scores))
print('alpha {0}일 때 5 폴드 세트의 평균 rmse:{1:.3f}'.format(param, avg_rmse))
# cross_val_score는 evaluation metric만 반환하므로 모델을 다시 학습하여 회귀계수 추출
model.fit(X_features_n, y_target_n)
if return_coeff:
# alpha에 따른 피처별 회귀계수를 Series로 변환하고 이를 DataFrame의 컬럼으로 추가
coeff = pd.Series(model.coef_, index=X_features_n.columns)
colname = 'alpha:'+str(param)
coeff_df[colname] = coeff
return coeff_df
# 라쏘에 사용될 alpha 파라미터의 값을 정의하고 get_linear_reg_eval() 함수 호출
lasso_alphas = [0.07, 0.1, 0.5, 1, 3]
coeff_lasso_df = get_linear_reg_eval('Lasso', params=lasso_alphas, X_features_n=X_features, y_target_n=y_target)
# 반환된 coeff_lasso_df를 첫 번째 컬럼순으로 내림차순 정렬해 회귀계수 DataFrame 출력
coeff_lasso_df.sort_values(by='alpha:'+str(lasso_alphas[0]), ascending=False) # by='alpha:0.07'
NOX 피처는 alpha가 0.07일 때부터 회귀계수가 0이다.
라쏘 회귀는 alpha의 크기가 증가함에 따라 일부 피처의 회귀계수는 아예 0으로 바뀌고 있다. 회귀계수가 0인 피처는 회귀식에서 제외되면서 피처 선택의 효과를 얻을 수 있다.
엘라스틱넷 회귀(L2 Norm+L1 Norm)
L2 규제와 L1 규제를 결합한 회귀.
○ 엘라스틱넷은 라쏘 회귀가 서로 상관관계가 높은 피처들의 경우에 이들 중에서 중요 피처만을 셀렉션하고 다른 피처들은 모두 회귀계수를 0으로 만드는 성향이 강하다. 특히 이러한 성향으로 인해 alpha값에 따라 회귀계수의 값이 급격히 변동할 수도 있는데, 엘라스틱넷 회귀는 이를 완화하기 위해 L2 규제를 라쏘 회귀에 추가한 것
○ 주요 생성 파라미터는 alpha, l1_ratio
비용함수는
이고, 이 식을 최소화하는 W를 찾는 것이 목표이다.
alpha 값에 따른 rmse와 회귀계수를 살펴보자.(코드에 l1_ratio를 0.7로 고정시켰는데, 이는 단순히 alpha값의 변화만 살피기 위해 미리 고정했음)
# 엘라스틱넷에 사용될 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_features_n=X_features, y_target_n=y_target)
# 반환된 coeff_elastic_df를 첫 번째 컬럼순으로 내림차순 정렬해 회귀계수 DataFrame 출력
coeff_elastic_df.sort_values(by='alpha:'+str(elastic_alphas[0]), ascending=False) # by='alpha:0.07'
alpha값에 따른 피처들의 회귀계수들 값이 라쏘보다는 상대적으로 0이 되는 값이 적음을 알 수 있다.
지금까지 공부한 회귀는 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)
편향과 분산은 한쪽이 높으면 한쪽이 낮아지는 트레이드오프 관계에 있다. 따라서 둘을 적절하게 조절하여 전체 오류가 가장 낮아지는 '골디락스' 지점을 찾는 것이 매우 중요하다. 즉, 편향-분산 트레이드오프는 머신러닝이 극복해야 할 가장 중요한 이슈 중 하나로 머신러닝 모델을 만들때 과소적합, 과적합 모델이 아닌 적절한 모델을 만들어야 한다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from sklearn.datasets import load_boston
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
# boston 데이터 세트 로드
boston = load_boston()
# boston 데이터 세트 DataFrame 변환
bostonDF = pd.DataFrame(boston.data, columns=boston.feature_names)
# boston 데이터 세트의 target array는 주택 가격. 이를 PRICE 컬럼으로 DataFrame에 추가
bostonDF['PRICE'] = boston.target
bostonDF.head()
변수 설명
CRIM: 지역별 범죄 발생률
ZN: 25,000평방 피트를 초과하는 거주 지역의 비율
INDUS: 비상업 지역 넓이 비율
CHAS: 찰스강에 대한 더미변수(강의 경계에 위치한 경우1, 아니면 0)
NOX:일산화질소 농도
RM: 거주할 수 있는 방 개수
AGE: 1940년 이전에 건축된 소유 주택의 비율
DIS: 5개 주요 고용센터까지의 가중 거리
RAD: 고속도로 접근 용이도
TAX: 10,000달러당 재산세율
PTRATIO: 지역의 교사와 학생 수 비율
B: 지역의 흑인 거주 비율
LSTAT: 하위 계층의 비율
bostonDF.info()
bostonDF.isnull().sum()
결측값은 없으며 피처들이 모두 float형이다.
다음으로 각 피처와 타겟값 사이의 상관관계를 살펴보자.(참고로 seaborn의 regplot() 함수는 x, y 축 값의 산점도와 함께 선형 회귀 직선을 그려줌)
# 2X4 subplots 이용. axs는 4*2개의 ax를 가짐
fig, axs = plt.subplots(figsize=(16,8), ncols=4, nrows=2)
lm_features = ['RM', 'ZN', 'INDUS','NOX', 'AGE', 'PTRATIO', 'LSTAT', 'RAD']
for i, feature in enumerate(lm_features):
row = int(i/4) # 나누기
col = i%4 # 나머지
sns.regplot(x=feature, y='PRICE', data=bostonDF, ax=axs[row][col])