교차검증 기초개념

2019-03-15

.

그림, 실습코드 등 학습자료 출처 : https://datascienceschool.net

# 교차검증

  • 모델의 성능을 얘기할때 가장 많이쓰는 기법 중에 하나이다.

  • 오버피팅을 막기위해 데이터를 전부쓰지 않고 일부를 잘라서 하나는 test set로 남겨놓는것을 train-test split이라고 한다. train 했을때 퍼포먼스와 test set으로 test 퍼포먼스가 비슷하게 나왔으면 그것은 오버피팅이 안되었다고 말 할 수 있다. 반대로 test 퍼포먼스가 train 퍼포먼스보다 차이가 크게 낮은 퍼포먼스가 나왔다면 오버피팅이라고 할 수 있다. 왜냐하면 train 데이터에만 최적화가 되어있는 것이 숫자로 나타났기 때문이다. 이런 것을 미리 검정해보자는 것이다.

  • 통상적으로 train 퍼포먼스보다 test 퍼포먼스가 조금 낮게 나오는 편이지만 R 스케어값도 결국에는 통계량이기 때문에 어떤 값이 나올지는 모르는 것이다. 만약에 train 데이터에서 찾아낸 규칙이 운좋게 test 데이터에 있는 놈들을 잘 따르고 있다면 test 퍼포먼스가 더 좋게 나올 수도 있는 것이다.

  • 만약에 오버피팅이 되어있으면 오버피팅이 안되게하는 정규화 방법을 써서 오버피팅을 원천 봉쇄시키는 방법이있고 다른 방법도 있다.

  • 경제학이나 의학에 적용하는 전통적인 선형회귀분석에서는 사실 데이터를 구하기가 매우 어려운 편이다. 그래서 데이터의 갯수가 적으면 오버피팅을 발생시키려고 해도 잘 안일어난다. 그러다보니 이런 전통적인 선형회귀분석에서는 오버피팅 걱정을 할 필요가 없었다. 그러다보니 statesmodels에서도 오버피팅을 검증하기 위한 교차검증 기능이 없다.

  • 현재로서는 그래서 scikit-learn의 교차검증 메서드를 이용하는게 통상적인 방법이다. 또는 미리 주어진 데이터에서 미리 test set을 분리해내는것이다.

  • 아래는 주어진 데이터에서 미리 test set을 분리하는 과정을 코딩한 것이다.

from sklearn.datasets import load_boston
import random

boston = load_boston()
dfX = pd.DataFrame(boston.data, columns=boston.feature_names)
dfy = pd.DataFrame(boston.target, columns=["MEDV"])
df = pd.concat([dfX, dfy], axis=1)

## 전체데이터의 70%는 train 데이터로 쓰고 
## 나머지 30% 데이터는 set을 해서 test 데이터로 분리하겠다
## 인덱스 리스트를 이용해서 train set과, test set을 분리한다.

N = len(df)
ratio = 0.7
np.random.seed(0)
idx_train = random.sample(list(np.arange(N)), np.int(ratio * N))
## 참고로 random sample은 중복을 허용하지 않는 랜덤 뽑기이고, random choice는 중복을 허용하는 랜덤뽑기이다.

idx_test = list(set(np.arange(N)).difference(idx_train))

df_train = df.iloc[idx_train]
df_test = df.iloc[idx_test]

print("df 길이 :", len(df))
print("df_train 길이 :",len(df_train))
print("df_test 길이 :", len(df_test))
df 길이 : 506
df_train 길이 : 354
df_test 길이 : 152
  • scikit-learn에는 train_test_split이라는 명령이 존재한다. 아래는 그 기능을 사용하는 예시로 한줄 코딩이면 train-test split이 가능하다.
from sklearn.model_selection import train_test_split

df_train, df_test = train_test_split(df, test_size=0.3, random_state=0)
df_train.shape, df_test.shape
((354, 14), (152, 14))
  • 그 다음에 나온 테크닉은 그냥 교차검증을 하다보니까 내가 예를들어 500개의 데이터가 있으면 400개로 모델을 만들고 나머지 100개로 점수를 구한점수를 통상 믿게된다. 근데 이것을 그냥 단정적으로 믿을 수 있냐가 문제이다. 왜냐하면 통상 모델의 퍼포먼스가 높아지려면 데이터가 많이 필요한데 통상 test 데이터로 따로 떼어놓은 데이터수가 상대적으로 매우 적은 편이다. 왜냐하면 test 데이터로 많이 뗴어 놓을 수록 train할 데이터가 없어지기 때문이다. 그렇게 되면 모델의 성능이 저하될 수도 있다. 그래서 가능한한 test 데이터는 조금만 뗴어놓는다. 통상 10 ~ 30 %정도..

  • 그런데 이렇게 적은 데이터수로 시험문제를 푼 점수갖고 이게 이 모델의 올바른 점수라고 단정할 수는 없다. 그래서 생각해낸 테크닉이 하나의 데이터세트를 k개의 셋트로 다시 분할해서 마지막 k세트는 test 데이터 세트로 쓰고 나머지는 모델링한 다음에 성능을 확인하고, 그 다음에 k-1번째를 test 데이터 세트로 쓰고 나머지는 모델링하고 이런 식으로 K개의 교차검증 성능을 평균하여 최종 교차검증 성능을 계산하는 방법을 쓰는데 그것을 kfold 교차검증이라고 한다.

  • 이 것을 극단적으로 하게 되면 1000개의 데이터가 있다고 하면 1개의 데이터만 test 용으로 두고 kfold를 하는 케이스도 있다. 그러면 시험을 1000번 보는 것이다. 이런식으로 하면 테스트 성능이 여러개가 생기니까 모델의 평균점수를 테스트 퍼포먼스로 볼 수 있다.

  • 이 kfold를 도와주는 클래스가 scikit-learn 패키지의 KFold 클래스이다. KFold 클래스에 숫자를 입력하면 kfold 제네레이터라는 객체를 만들어준다. shuffle=True 옵션은 셔플설정을 안해주면 데이터 앞에서 순서대로 자르게 되는데 셔플 true를 해주면 일단 랜덤으로 셔플을하고 잘라준다는 얘기이다. 이렇게 만든 객체를 cv.split(df) 이런식으로 호출 할 수 있다. 아래는 그것의 예시이다.

from sklearn.model_selection import KFold
from sklearn.metrics import r2_score

scores = np.zeros(5)
cv = KFold(5, shuffle=True, random_state=0)
for index, (idx_train, idx_test) in enumerate(cv.split(df)):
    df_train = df.iloc[idx_train]
    df_test = df.iloc[idx_test]
    
    model = sm.OLS.from_formula("MEDV ~ " + "+".join(boston.feature_names), data=df_train)
    result = model.fit()
    
    prediction = result.predict(df_test)
    rsquared = r2_score(df_test.MEDV, prediction)
    
    scores[index] = rsquared

scores
array([0.58922238, 0.77799144, 0.66791979, 0.6680163 , 0.83953317])
  • cv.split(df)를 호출하면 이터레이터가 반환된다. 이터레이터는 참고로 next라는 메서드를 소장하고 있어서 for문에 넣고 돌리게 되면 계속 무언가를 하나씩 출력하는 것이다. 이 이터레이터를 만드는 얘가 제네레이터이다. 여기서 cv 객체가 제네레이터라고 생각하면 된다.

  • 이렇게 kfold된 데이터를 바탕으로 모델을 피팅하고 train 퍼포먼스와 test 퍼포먼스를 생성한다. 이 스코어들을 scores[i]라는 배열 안에다 저장을 하게 된다. 그리고 scikit-learn의 metrics 서브패키지에서 제공하는 예측 성능을 평가하기 위한 다양한 함수를 통해 점수를 도출 할 수 있다.

  • 위와 같이 교차검증을 반복하는 코드를 더 간단하게 만들어주는 함수가 있다. 바로 cross_val_score이다. 다만 이 명령은 scikit-learn에서 제공하는 모형만 사용할 수 있기 때문에 statsmodels의 모형 객체를 사용하려면 아래와 같이 scikit-learn의 RegressorMixin으로 wrapper class를 만들어주어야 한다.

  • 이 래퍼 클래스와 cross_val_score 명령을 사용하면 교차검증 스코어를 아래와 같이 간단하게 도출할 수 있다.

from sklearn.base import BaseEstimator, RegressorMixin
from sklearn.model_selection import cross_val_score
import statsmodels.formula.api as smf
import statsmodels.api as sm

class StatsmodelsOLS(BaseEstimator, RegressorMixin):
    def __init__(self, formula):
        self.formula = formula
        self.model = None
        self.data = None
        self.result = None
        
    def fit(self, dfX, dfy):
        self.data = pd.concat([dfX, dfy], axis=1)
        self.model = smf.ols(self.formula, data=self.data)
        self.result = self.model.fit()
        
    def predict(self, new_data):
        return self.result.predict(new_data)

model = StatsmodelsOLS("MEDV ~ " + "+".join(boston.feature_names))
cv = KFold(5, shuffle=True, random_state=0)
cross_val_score(model, dfX, dfy, scoring="r2", cv=cv)
array([0.58922238, 0.77799144, 0.66791979, 0.6680163 , 0.83953317])