과적합과 일반화
다음은 Underfitting 과 Overfitting의 예시이다.
회귀(Regression): train에 맞추려고 하다보니 빨간선처럼 이상한 함수가 만들어 졌다.
분류(Classification): train과 vallidation모두 성능이 좋지 못하다. 이상치 몇개를 무시하여 선을 만들어도 train성능은 좋아져도 test성능은 좋지않다.
<정리>
너무 과하게 맞추거나 너무 안맞추는 것은 좋지않으니 일반적인 특성들을 맞출 수 있도록 하는게 가장 best이다.
그럼 이런 Underfitting 과 Overfitting이 일어나는 원인은 무엇일까?
Overfitting(과대적합)의 원인
Underfitting(과소적합)의 원인
위에서 언급되는 규제 하이퍼파라미터란 무엇이길래 원인을 해결하는 방법으로 언급이 되는 것일까?
개념정리는 됐다! 이제 예제를 보자.
규제 하이퍼파라미터(모델의 문제를 조정)를 하기 위해서는 모델이 oerfitting이 났는지 underfitting이 났는지 알아한다.
1) 데이터셋을 정하고 분리
2) 모델 선정 후 모델링
from sklearn.datasets import load_breast_cancer # 유방암데이터셋
from sklearn.model_selection import train_test_split # 데이터셋 분리
data = load_breast_cancer()
X, y = data.data, data.target
# 데이터셋 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
3) 최적의 하이퍼파라미터 찾기
3-1 최적의 max_depth 찾기
from sklearn.tree import DecisionTreeClassifier # 모델선정 - 분류-범주형
from sklearn.metrics import accuracy_score # 평가방법
# max_depth: None(default)-모든 leaf node들의 gini 계수가 0이 될때까지(하나의 노드에 하나의 클래스만 있을때 까지) 분기
max_depth_candidate = [1, 2, 3, 4, 5, None]
# 검증 결과를 담을 리스트
train_acc_list = []
test_acc_list = []
# 모델링
for max_depth in max_depth_candidate:
# 객체생성
tree = DecisionTreeClassifier(max_depth=max_depth, random_state=0)
# 학습
tree.fit(X_train, y_train)
# 판정
pred_train = tree.predict(X_train)
pred_test = tree.predict(X_test)
#평가
train_acc_list.append(accuracy_score(y_train, pred_train))
test_acc_list.append(accuracy_score(y_test, pred_test))
모델링 결과확인
import pandas as pd
result_df = pd.DataFrame({
"train":train_acc_list,
"test":test_acc_list
}, index=max_depth_candidate)
result_df
각 깊이 별로 train set,test set의 정확도를 알 수 있다.
그럼 여기서 max_depth는?
result_df.plot()
위의 DataFrame을 line plot로 나타낸 결과 train과 test set 결과가 모두 적당한 3이 최적의 max_depth(트리의 최대 깊이)인 것을 알 수 있다.
!여기서 잠깐!
최적의 파라미터 (max_depth)를 찾기 위해 항상 결과를 담을 리스트를 만들고 모델링결과를 리스트에 담아서 계속확인해야할까?
Grid Search 를 이용한 하이퍼파라미터 튜닝 자동화
<종류>
(Grid Search 방식 / Random Search 방식)
1. Grid Search 방식
sklearn.model_selection.GridSearchCV 사용
시도해볼 하이퍼파라미터들을 지정하면 모든 조합에 대해 교차검증 후 제일 좋은 성능을 내는 조합을 찾아준다.
적은 조합의 경우는 괜찮지만 시도할 하이퍼파라미터와 값들이 너무 많아지면 시간이 많이 걸린다.
GridSearch 매개변수 결과조회
메소드
결과조회
그럼 이제 예시를 통해서 확인해보자!
테스트셋은 위에서 사용한 테스트셋을 사용한다.
from sklearn.model_selection import GridSearchCV ##Gridsearch 자동화 시 전부테스트할때 사용
from sklearn.tree import DecisionTreeClassifier # 테스트모델은 DecisionTree모델을 사용
# 모델생성
tree = DecisionTreeClassifier(random_state=0)
# 파라미터 후보 딕셔너리
# key: 파라미터이름, value: 후보 리스트
params = {
"max_depth":[1,2,3,4,5], # range(1,6)도 가능
"max_leaf_nodes":[3,4,5,6,7,8,9,10]
}
# 5 * 8
gs = GridSearchCV(tree, # 하이퍼파라미터를 찾을 대상 모델객체
params, # 하이퍼파라미터 후보 딕셔너리
scoring="accuracy", # 평가지표(분류: accuracy가 기본.)
cv=4, # cross validation의 fold 개수.
n_jobs=-1 # 사용할 cpu개수. -1: 다사용.
)
# 하이퍼파라미터 찾기(자동화모델에 학습)
gs.fit(X_train, y_train)
=> search안에 DecisionTree 객체를 넣어서 최적의 하이퍼파라미를 찾는데 이때 하이퍼파라미터를 찾기위한 분리된 데이터셋을 넣어 학습시킨다.
## 결과 조회
### 가장 성능 좋은 조합의 하이퍼파라미터가 무엇인지 알려준다.
gs.best_params_
# 결과
{'max_depth': 2, 'max_leaf_nodes': 4}
### best_params_ 모델의 평가점수가 얼마가 나왔는지
gs.best_score_
#결과
0.9248809733733028
# best_params_로 재학습시킨 모델
best_model = gs.best_estimator_
best_model
=> 이렇게 GridSerch로 성능이 가장좋은 파라미터를 찾아 찾은 최적의파라미터로 재학습하기 위해 best_model에 저장한다.
그럼 최적의 모델을 제대로 찾은게 맞는지 확인해 보자.
from sklearn.metrics import accuracy_score
### best model 로 최종 평가
pred_test = best_model.predict(X_test)
accuracy_score(y_test, pred_test)
# 결과
0.8881118881118881
테스트모델로 검증한 결과와 학습시켰던 자동화객체에 testset을 넣고 테스트한 결과를 비교해본다.
pred_test2 = gs.predict(X_test)
accuracy_score(y_test, pred_test2)
# 결과
0.8881118881118881
<결론>
동일한 결과가 나온것을 확인할 수 있고 최적의 파라미터를 잘찾은 것을 알 수 있다.
klearn.model_selection.RandomizedSearchCV 사용
GridSeach와 동일한 방식으로 사용
단, 모든 조합을 테스트하지않고 랜덤하게 몇개의 조합을 선택하여 테스트한다.
Initializer 매개변수
메소드
결과 조회 속성
예제로 확실하게 알아보자!
from sklearn.model_selection import RandomizedSearchCV
import numpy as np
tree3 = DecisionTreeClassifier(random_state=0)
params3 = {
"max_depth":range(1, 6),
"max_leaf_nodes":range(3, 31),
"max_features":np.arange(0.1, 1.1, 0.1) # 학습시 사용할 컬럼의 비율
}
rs = RandomizedSearchCV(tree3, # 모델
params3, # 하이퍼파라밑터 후보
n_iter=60, # 테스트해볼 조합의 개수.
scoring="accuracy",
cv=4,
n_jobs=-1
)
rs.fit(X_train, y_train) # 찾기
print("best score:", rs.best_score_)
print("best parameter:", rs.best_params_)
# 결과
best score: 0.9530285663904072
best parameter: {'max_leaf_nodes': 25, 'max_features': 0.4, 'max_depth': 5}
# RandomizedSearch에 찾은 하이퍼파라미터를 기준으로 그 근처의 값들을 좀더 세분화해서 찾는다.
# 세분화해서 찾기.
params4 = {
"max_leaf_nodes":[24, 25, 26, 17, 28],
"max_depth":[2, 3, 4],
"max_features":[0.2, 0.3, 0.4, 0.5, 0.6] # 학습시 지정한 비율의 feature만 사용.
}
gs4 = GridSearchCV(DecisionTreeClassifier(random_state=0),
params4,
scoring='accuracy',
cv=4,
n_jobs=-1)
gs4.fit(X_train, y_train)
### 결과 확인
print(gs4.best_score_)
print(gs4.best_params_)
# 결과
0.9553650149885382
{'max_depth': 3, 'max_features': 0.4, 'max_leaf_nodes': 24}
# 재학습
bm = gs4.best_estimator_
pred_test = bm.predict(X_test)
accuracy_score(y_test, pred_test)
# 결과
0.9230769230769231
파이프라인 (Pipeline)
<파이프라인 종류>
!여기서 잠깐!
변환기가 뭐고 추정기가 뭔데??
변환기(Estimator): 데이터를 학습하고 머신러닝 알고리즘(모델)들을 구현한 클래스이다.
fit(), predict()
추정기(Transformer): 데이터를 전처리하는 클래스들로 데이터셋의 값을 변환한다.
fit(), transform() fit_transform()
다시이어서 파이프라인으로 ~
이건 예제를 봐야 이해가 확실이 되겠다.
빨리 예제를 보자.
from sklearn.datasets import load_breast_cancer # 데이터 셋 로드
from sklearn.model_selection import train_test_split, GridSearchCV # 테스트셋을 나누기, 시도해볼 하이퍼파라미터 전체시도
from sklearn.preprocessing import StandardScaler # 전처리
from sklearn.svm import SVC # 모델선정
from sklearn.pipeline import Pipeline # 파이프라인
# 1. 데이터 로드
X, y = load_breast_cancer(return_X_y=True) # 데이터와 레이블을 한번에 X,y에 할당하는 부분
# 2. 데이터셋 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
# 3. 파이프라인 정의
# 리스트로 작성(이름, 생성할 객체)
steps = [
("scaler", StandardScaler()), # 첫번째 프로세스 (전처리)
("svm", SVC(random_state=0)) # 두번째 프로세스 (사용모델)
]
# 파이프라인 생성
pl = Pipeline(steps, verbose=True) # 리스트를 대입
# verbose=True는 작업의 진행 상황을 자세하게 출력하고, verbose=False는 출력하지 않는다는 것을 의미
# verbose: 실행 로그(기록)을 출력 => 어떤 단계를 실행하고 있는지, 실행에 걸린 시간 등을 출력
# 파이프라인에 넣어서 모델생성 및 학습
pl.fit(X_train, y_train)
# 추론
pred_train = pl.predict(X_train) # 여기서는 학습한것을 기반으로 변환해서 predict한다.
pred_test = pl.predict(X_test)
# 평가
from sklearn.metrics import accuracy_score # 평가지표
accuracy_score(y_train, pred_train), accuracy_score(y_test, pred_test)
# 결과
(0.9929577464788732, 0.958041958041958)
GridSearch에서 Pipeline사용
<순서>
여기에서 PCA를 사용하는데 그 이유는?
PCA를 Pipeline에 포함시키는 이유는 데이터의 차원을 축소하여 모델의 복잡성을 줄이고 예측 성능을 향상시키기 위해서이며, Pipeline은 데이터 전처리와 모델 학습을 효율적으로 처리하고 모델 튜닝을 수행하는 데 도움을 주기때문이다.
from sklearn.decomposition import PCA #차원 축소 -> feature를 줄여준다.
pca = PCA(n_components=2) # feature를 몇개로 줄일지 -> feature extraction
# train을 학습 및 변환하여 차원축소해서 저장
X_trained_pca = pca.fit_transform(X_train)
X_trained_pca.shape
# 결과
(426, 2) # 2개로 Feature이 축소된것을 확인할 수 있다.
한번 시각화하여 데이터들이 어떻게 분포되어있는지 확인해 보자. (산점도 사용)
import matplotlib.pyplot as plt
plt.scatter(X_trained_pca[:, 0], X_trained_pca[:, 1], alpha=0.2)
plt.show()
GridSearh에 넣기 전에 데이터셋의 차원을 PCA를 통해 축소하였으니 이제 파이프라인을 생성하자.
# 1. 파이프라인 생성
steps = [
("scaler", StandardScaler()), # 전처리
("pca", PCA()), # 전처리(차원축소)
("svm", SVC(random_state=0)) # 모델링
]
pl2 = Pipeline(steps, verbose=True)
# 2. GridSearchCV생성
# key: 하이퍼파라미터 이름 (파이프라인에등록한이름__하이퍼파라미터이름)
# value: 후보-리스트
params = {
"pca__n_components":[5, 10, 15, 20, 25],
"svm__C":[0.001, 0.01, 0.1, 0.5, 1],
"svm__gamma":[0.001, 0.01, 0.1, 0.5, 1]
}
gs = GridSearchCV(pl2, # pipeline
params,
scoring='accuracy',
cv=4,
n_jobs=-1
)
gs.fit(X_train, y_train)
# 최적의 하이퍼파라미터 확인
gs.best_params_
# 결과
{'pca__n_components': 5, 'svm__C': 1, 'svm__gamma': 0.1}
# 최적의 하이퍼파라미터의 accuracy
gs.best_score_
# 결과
0.9765914300828777
# 가장좋은 것으로 재학습
best_model = gs.best_estimator_
# 해당 모델로 다시 학습 및 추론
pred_test = best_model.predict(X_test)
accuracy_score(y_test, pred_test)
# 결과
0.9440559440559441
지금까지는 데이터셋의 컬럼들이 모두 같은 전처리를 사용했다. 그런데 항상 같은 전처리를 사용하지 않을때도 있다.
그럼 데이터셋의 컬럼들마다 다른 전처리를 해야 하는 경우에는 어떻게 해야할까?
바로
ColumnTransformer를 사용한다.
ColumnTransformer
데이터셋의 컬럼들(Feature)마다 다른 전처리를 해야하는 경우 사용한다.
연속형 feature => Feature scaling을 범주형은 one hot encoding이나 labelencoding을 해야한다.
하나의 데이터셋을 구성하는 feature(커럼들)에 대해 서로 다른 전처리 방법이 필요할 때 개별적으로 나눠서 처리하는 것은 좋지않다.
=> 그런데 ColumnTransformer를 사용하면 feature별로 어떤 전처리를 할지 정의할 수 있다.
sklearn.compose.ColumnTransformer 이용
sklearn.compose.make_column_transformer 이용
예제를 통해 이해하기!
아래는 실습을 위한 데이터 프레임 생성과정이다.
1. 데이터 프레임 생성
import pandas as pd
import numpy as np
df = pd.DataFrame({
"gender":['남성', '여성', '여성', '여성', '여성', '여성', '남성', np.nan], # 범주형
"blood_type":["B", "B", "O", "AB", "B", np.nan, "B", "A"], # 범주형
"tall":[183.21, 175.73, np.nan, np.nan, 171.18, 181.11, 168.83, 193.99], # 연속형
"weight":[82.11, 62.45, 52.21, np.nan, 56.32, 48.93, 63.64, 102.38] # 연속형
}) # 4개의 컬럼을 줌
df
# 서로다른 전처리 쉽게할 수 있도록 , ColumnTransformer를 쉽게 생성할 수 있도록 도와주는 것.
from sklearn.compose import ColumnTransformer, make_column_transformer
from sklearn.impute import SimpleImputer # 결측치 처리
from sklearn.preprocessing import OneHotEncoder, StandardScaler # 범주형전처리, 연속형전처리
from sklearn.pipeline import Pipeline # 파이프라인
# 결측치 처리 - 범주형: 최빈값, 연속형: 평균
imputer_tf = ColumnTransformer([
("category_imputer", SimpleImputer(strategy="most_frequent"), ["gender", "blood_type"]),
("continuous_imputer", SimpleImputer(strategy="mean"), ["tall", "weight"])
],)
# 학습
imputer_tf.fit(df)
# 변환 된 feature의 이름.
imputer_tf.get_feature_names_out()
# 결과
array(['category_imputer__gender', 'category_imputer__blood_type',
'continuous_imputer__tall', 'continuous_imputer__weight'],
dtype=object)
# 변환
result = imputer_tf.transform(df)
result
# 결과
array([['남성', 'B', 183.21, 82.11],
['여성', 'B', 175.73, 62.45],
['여성', 'O', 179.00833333333335, 52.21],
['여성', 'AB', 179.00833333333335, 66.86285714285714],
['여성', 'B', 171.18, 56.32],
['여성', 'B', 181.11, 48.93],
['남성', 'B', 168.83, 63.64],
['여성', 'A', 193.99, 102.38]], dtype=object)
# 번주형-원핫인코딩, 연속형-standardScaling
preprocess_tf = ColumnTransformer([
("ohe", OneHotEncoder(), [0,1]),
('scaler', StandardScaler(), [2,3]) # ndarray의 경우 feature index번호로
], remainder='passthrough')
# remainder='passthrough': 변환기가 적용되지 않은 열을 그대로 유지할 수 있다.
# 여기 코드에서는 모든 열ㅇ르 적용했지때문에 지워도 달라지는 것은 없다.
# 학습
preprocess_tf.fit(result) # 결측치처리한 겂 넣기
preprocess_tf.get_feature_names_out()
# 결과
array(['ohe__x0_남성', 'ohe__x0_여성', 'ohe__x1_A', 'ohe__x1_AB', 'ohe__x1_B',
'ohe__x1_O', 'scaler__x2', 'scaler__x3'], dtype=object)
# 변환
result2 = preprocess_tf.transform(result)
result2
# 결과
array([[ 1. , 0. , 0. , 0. , 1. ,
0. , 0.57840648, 0.92550488],
[ 0. , 1. , 0. , 0. , 1. ,
0. , -0.4512993 , -0.26786139],
[ 0. , 1. , 0. , 0. , 0. ,
1. , 0. , -0.88943161],
[ 0. , 1. , 0. , 1. , 0. ,
0. , 0. , 0. ],
[ 0. , 1. , 0. , 0. , 1. ,
0. , -1.07765777, -0.63995372],
[ 0. , 1. , 0. , 0. , 1. ,
0. , 0.28931796, -1.08852832],
[ 1. , 0. , 0. , 0. , 1. ,
0. , -1.40116159, -0.19562813],
[ 0. , 1. , 1. , 0. , 0. ,
0. , 2.06239422, 2.15589828]])
# 파이프라인 으로 묶기
preprocess_pipeline = Pipeline([
("imputer", imputer_tf),
("preprocess", preprocess_tf)
])
preprocess_pipeline.fit_transform(df)
# 결과
array([[ 1. , 0. , 0. , 0. , 1. ,
0. , 0.57840648, 0.92550488],
[ 0. , 1. , 0. , 0. , 1. ,
0. , -0.4512993 , -0.26786139],
[ 0. , 1. , 0. , 0. , 0. ,
1. , 0. , -0.88943161],
[ 0. , 1. , 0. , 1. , 0. ,
0. , 0. , 0. ],
[ 0. , 1. , 0. , 0. , 1. ,
0. , -1.07765777, -0.63995372],
[ 0. , 1. , 0. , 0. , 1. ,
0. , 0.28931796, -1.08852832],
[ 1. , 0. , 0. , 0. , 1. ,
0. , -1.40116159, -0.19562813],
[ 0. , 1. , 1. , 0. , 0. ,
0. , 2.06239422, 2.15589828]])
####### GridSearch적용
from sklearn.tree import DecisionTreeClassifier # 모델선정
from sklearn.model_selection import GridSearchCV # 가장좋은 하이퍼파라미터 찾기
# 연속형 파이프라인 생성
num_preprocess = Pipeline([
("imputer", SimpleImputer()), ########## mean, median 적용
("scaler", StandardScaler())
])
# 범주형 파이프라인 생성
cate_preprocess = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("ohe", OneHotEncoder(handle_unknown="ignore"))
])
# 연속형과 범주형 묶기
preprocessor = ColumnTransformer([
("categorical", cate_preprocess, ['gender', "blood_type"]),
("continuous", num_preprocess, ['tall', 'weight']),
])
# 묶은 것을 포함하여 적용할 모델과 새로운 파이프라인 생성
pl = Pipeline([
('preprocess', preprocessor),
('model', DecisionTreeClassifier(random_state=0))
])
# => 4번에서 한 과정이지만 보통 범주형, 연속형으로 나눠 pipeline을 생성 후 ColumnTransformer로 묶어주는 전략을 많이 쓴다.
# GridSearch 파라미터 후보 딕셔너리
# key: 파라미터이름, value: 후보 리스트
param = {
"preprocess__continuous__imputer__strategy":["mean", "median"],
"model__max_depth":range(2, 5)
}
y = [1, 0, 0, 0, 1, 1, 1, 0] # 가상의 데이터
# GridSearchCV객체 생성
gs = GridSearchCV(pl, param, scoring='accuracy', cv=3, verbose=True)
# 헉습
gs.fit(df, y)
# 추론
gs.predict(df)
# 결과
array([1, 1, 0, 0, 1, 1, 1, 0])
# 남성과 Nan이였던 부분은 0으로 여성은 1로 표현된것을 알 수 있다.