데이콘이나 캐글 같은 경진대회에서 어떤 예측값을 제출하느냐에 따라 순위가 몇 단계나 출렁이곤 한다.
그렇기 때문에 어떤 데이터에 대해서도 견고한 예측값을 제공하는 모델을 선택하는 것이 중요한 이슈라고 할 수 있다.
모델을 어떻게 학습하냐에 따라 예측값도 천차만별이다. 좋은 결과를 위해서는 과적합, 과소 적합된 모델보다 균형 있게 학습된 모델을 선택하는 것이 가장 중요할 것이다.
쉬운 이해를 돕기 위해 인간으로 비유하자면
- 현재의 고정된 관념을 너무 학습하여 미래의 현상에도 고정 관념에 사로잡힌 사람
- 과적합 - 현재도 잘 이해하지 못하고 있으며 미래의 현상도 잘 이해하지 못하는 사람
- 과소적합 - 현재도 잘 이해하며 미래의 현상도 잘 예측하는 사람
- 일반화
K-fold 교차검증을 통해 비교적 일반화(generalization)된 모델을 선택하는 데 도움을 받을 수 있다.
K-Fold 교차검증, K-Fold Cross Validation
흐름
머신러닝 학습 시 최적의 파라메터를 찾는 것은 중요한 이슈라고 할 수 있다. 실제로 캐글과 같은 경연 플랫폼에서는 파라메터 세팅이 조금만 달라도 결과가 달라지고 이것은 순위의 변동으로도 이어진다.
위 그림 속 CV 흐름도를 요약하자면
- CV(교차검증)를 실시하여 여러 머신러닝 기법 모델의 전반적인 성능을 비교한 후 하나의 기법을 선택
- 선택된 기법을 활용하여 데이터에 알맞은 최적의 파라메터 탐색
K-Fold Cross Validation 단계 순서
- 데이터의 상황에 따라 K개의 Group으로 알맞게 나눔
- K-1개 그룹의 데이터를 훈련 데이터로 사용하여 모델을 훈련
- 데이터의 나머지 부분에 대해 검증 (즉, 정확도와 같은 성능 측정값을 계산하는 데 테스트 세트로 사용됨)
- 검증 데이터 그룹을 달리하여 2,3 과정을 반복 (즉, 총 K번의 검증이 이루어짐)
- K번 검증된 결과의 평균을 측정 (예를 들면 K번의 정확도 결과를 평균냄)
* 해당 과정에서 중요한 점은 Data Leakage가 없도록 훈련 데이터와 검증 데이터를 명확히 구분해야 한다는 점이다.
Cross Validation iterators
데이터 세트의 구조나 특성은 각 상황에 따라 천차만별이기 때문에 각 상황에 맞는 반복자(iterators)를 사용하여야 한다.
크게 네 가지 정도로 구분 가능하다.
- 데이터들이 독립적이고 동일한 분포를 가진 경우
- 데이터의 분포가 다른 경우
- 데이터가 그룹화되어 있는 경우
- 데이터가 시계열 데이터인 경우
데이터들이 독립적이고 동일한 분포를 가진 경우
-
K-Fold
K-Fold는 모든 샘플을 동일한 크기(가능한 경우)로 folds라고 하는 샘플 그룹으로 나눈다. 각 반복마다 K-1 folds를 사용하여 학습하고, 남은 fold는 테스트에 사용한다.
import numpy as np
from sklearn.model_selection import KFold
X = ["a", "b", "c", "d"]
kf = KFold(n_splits=2)
for train, test in kf.split(X):
print("%s %s" % (train, test))
>
[2 3] [0 1]
[0 1] [2 3]
-
Repeated K-Fold
Repeated K-Fold는 K-Fold를 여러 번 반복하는 것. 각 반복마다 서로 다른 분할이 발생한다.
import numpy as np
from sklearn.model_selection import RepeatedKFold
X = np.array([[1, 2], [3, 4], [1, 2], [3, 4]])
random_state = 12883823
rkf = RepeatedKFold(n_splits=2, n_repeats=2, random_state=random_state)
for train, test in rkf.split(X):
print("%s %s" % (train, test))
>
[2 3] [0 1]
[0 1] [2 3]
[0 2] [1 3]
[1 3] [0 2]
-
Leave One Out (LOO)
LOO는 간단한 교차검증이다. 각 학습 세트는 하나의 샘플을 제외한 모든 샘플을 추출하여 생성된다. 테스트 세트는 제외된 샘플이다. 훈련 과정에서 데이터 샘플 하나만 제거되므로 데이터 수가 적을 때 많은 데이터의 낭비를 막기 위해서 사용할 수 있다.
from sklearn.model_selection import LeaveOneOut
X = [1, 2, 3, 4]
loo = LeaveOneOut()
for train, test in loo.split(X):
print("%s %s" % (train, test))
>
[1 2 3] [0]
[0 2 3] [1]
[0 1 3] [2]
[0 1 2] [3]
-
Leave P Out (LPO)
Leave P Out은 전체 데이터 세트에서 p개의 샘플을 제거하여 가능한 모든 훈련/검증 데이터 세트를 생성하므로 LOO와 매우 유사하다.
from sklearn.model_selection import LeavePOut
X = np.ones(4)
lpo = LeavePOut(p=2)
for train, test in lpo.split(X):
print("%s %s" % (train, test))
>
[2 3] [0 1]
[1 3] [0 2]
[1 2] [0 3]
[0 3] [1 2]
[0 2] [1 3]
[0 1] [2 3]
-
Shuffle & Split
Shuffle & Split은 샘플을 먼저 섞은 다음 훈련/검증 데이터 세트를 나눈다. random_state 값을 정해두어 결과를 재현하도록 제어 가능하다.
from sklearn.model_selection import ShuffleSplit
X = np.arange(10)
ss = ShuffleSplit(n_splits=5, test_size=0.25, random_state=0)
for train_index, test_index in ss.split(X):
print("%s %s" % (train_index, test_index))
>
[9 1 6 7 3 0 5] [2 8 4]
[2 9 8 0 6 7 4] [3 5 1]
[4 5 1 0 6 9 7] [2 3 8]
[2 7 5 8 0 3 4] [6 1 9]
[4 1 0 6 8 9 3] [5 2 7]
데이터의 분포가 다른 경우
-
Stratified K-Fold
Stratified K-Fold는 층화 된 folds를 반환하는 기존 K-Fold의 변형된 방식이다. 각 집합에는 전체 집합과 거의 동일하게 클래스의 표본 비율이 포함된다. 불균형 클래스의 경우 사용.
from sklearn.model_selection import StratifiedKFold, KFold
import numpy as np
X, y = np.ones((50, 1)), np.hstack(([0] * 45, [1] * 5))
skf = StratifiedKFold(n_splits=3)
for train, test in skf.split(X, y):
print('train - {} | test - {}'.format(
np.bincount(y[train]), np.bincount(y[test])))
>
train - [30 3] | test - [15 2]
train - [30 3] | test - [15 2]
train - [30 4] | test - [15 1]
kf = KFold(n_splits=3)
for train, test in kf.split(X, y):
print('train - {} | test - {}'.format(
np.bincount(y[train]), np.bincount(y[test])))
>
train - [28 5] | test - [17]
train - [28 5] | test - [17]
train - [34] | test - [11 5]
-
Stratified Shuffle & Split
Stratified Shuffle & Split은 계층화된 분할을 반환하는 Shuffle & Split의 변형으로, 전체 집합에서와 같이 각 대상 클래스에 대해 동일한 비율을 보존하여 분할을 생성.
import numpy as np
from sklearn.model_selection import StratifiedShuffleSplit
X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4]])
y = np.array([0, 0, 0, 1, 1, 1])
sss = StratifiedShuffleSplit(n_splits=5, test_size=0.5, random_state=0)
sss.get_n_splits(X, y)
>
5
print(sss)
>
StratifiedShuffleSplit(n_splits=5, random_state=0, ...)
for train_index, test_index in sss.split(X, y):
print("TRAIN:", train_index, "TEST:", test_index)
X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]
>
TRAIN: [5 2 3] TEST: [4 1 0]
TRAIN: [5 1 4] TEST: [0 2 3]
TRAIN: [5 0 2] TEST: [4 3 1]
TRAIN: [4 1 0] TEST: [2 3 5]
TRAIN: [0 5 1] TEST: [3 4 2]
데이터가 그룹화되어 있는 경우
-
Group K-Fold
Group K-Fold는 동일한 그룹이 훈련 및 검증 데이터 셋에 동시에 포함되지 않도록 하는 방법.
from sklearn.model_selection import GroupKFold
X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 8.8, 9, 10]
y = ["a", "b", "b", "b", "c", "c", "c", "d", "d", "d"]
groups = [1, 1, 1, 2, 2, 2, 3, 3, 3, 3]
gkf = GroupKFold(n_splits=3)
for train, test in gkf.split(X, y, groups=groups):
print("%s %s" % (train, test))
>
[0 1 2 3 4 5] [6 7 8 9]
[0 1 2 6 7 8 9] [3 4 5]
[3 4 5 6 7 8 9] [0 1 2]
-
Leave One Group Out
Leave One Group Out은 LOO와 비슷한 방법이다. LOO에서는 하나의 데이터를 검증 데이터로 남겨두고 나머지 데이터로 학습 데이터를 구성했다면 LOGO에서는 하나의 그룹을 남겨두고 나머지 그룹으로 학습 데이터를 구성한다고 생각하면 될 것 같다.
from sklearn.model_selection import LeaveOneGroupOut
X = [1, 5, 10, 50, 60, 70, 80]
y = [0, 1, 1, 2, 2, 2, 2]
groups = [1, 1, 2, 2, 3, 3, 3]
logo = LeaveOneGroupOut()
for train, test in logo.split(X, y, groups=groups):
print("%s %s" % (train, test))
>
[2 3 4 5 6] [0 1]
[0 1 4 5 6] [2 3]
[0 1 2 3] [4 5 6]
-
Leave P Groups Out
Leave P Groups Out은 LOGO와 비슷하며 P개의 그룹을 남겨두고 나머지 그룹을 훈련 데이터로 구성한다.
from sklearn.model_selection import LeavePGroupsOut
X = np.arange(6)
y = [1, 1, 1, 2, 2, 2]
groups = [1, 1, 2, 2, 3, 3]
lpgo = LeavePGroupsOut(n_groups=2)
for train, test in lpgo.split(X, y, groups=groups):
print("%s %s" % (train, test))
>
[4 5] [0 1 2 3]
[2 3] [0 1 4 5]
[0 1] [2 3 4 5]
-
Group Shuffle Split
Group Shuffle Split은 Shuffle & Split과 LPGO의 조합으로 동작. 각 분할에 대해 그룹의 하위 집합이 보류되는 랜덤화 된 분할 시퀀스를 생성.
from sklearn.model_selection import GroupShuffleSplit
X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 0.001]
y = ["a", "b", "b", "b", "c", "c", "c", "a"]
groups = [1, 1, 2, 2, 3, 3, 4, 4]
gss = GroupShuffleSplit(n_splits=4, test_size=0.5, random_state=0)
for train, test in gss.split(X, y, groups=groups):
print("%s %s" % (train, test))
>
[0 1 2 3] [4 5 6 7]
[2 3 6 7] [0 1 4 5]
[2 3 4 5] [0 1 6 7]
[4 5 6 7] [0 1 2 3]
데이터가 시계열 데이터인 경우
-
Time Series Split
Time Series Split은 K-Fold의 변형으로, 첫 번째 fold는 훈련 데이터 세트로, 두 번째 fold는 검증 데이터 세트로 분할.
기존의 교차 검증 방법과 달리, 연속적 훈련 데이터 세트는 그 이전의 훈련 및 검증 데이터를 포함한 상위 집합이다. 해당 검증 방법은 고정 시간 간격으로 관측된 시계열 데이터 샘플을 교차검증할 때 사용한다.
from sklearn.model_selection import TimeSeriesSplit
X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4]])
y = np.array([1, 2, 3, 4, 5, 6])
tscv = TimeSeriesSplit(n_splits=3)
print(tscv)
>
TimeSeriesSplit(gap=0, max_train_size=None, n_splits=3, test_size=None)
for train, test in tscv.split(X):
print("%s %s" % (train, test))
>
[0 1 2] [3]
[0 1 2 3] [4]
[0 1 2 3 4] [5]
요약 및 결론
요약
-
데이터들이 독립적이고 동일한 분포를 가진 경우
KFold, RepeatedKFold, LeaveOneOut(LOO), LeavePOutLeaveOneOut(LPO) -
데이터의 분포가 다른 경우
StratifiedKFold, RepeatedStratifiedKFold, StratifiedShuffleSplit -
데이터가 그룹화되어 있는 경우
GroupKFold, LeaveOneGroupOut, LeavePGroupsOut, GroupShuffleSplit -
데이터가 시계열 데이터인 경우
TimeSeriesSplit
결론
다양한 K-Fold 데이터 분할 방법을 익혀두고 상황에 알맞은 방법으로 데이터를 분할할 줄 알아야 하겠다.
참고
https://scikit-learn.org/stable/modules/cross_validation.html