Skip to content

Feature Importance

jaeaehkim edited this page Jun 9, 2022 · 7 revisions

Motivation

  • 잘못된 절차 : 특정 Data 선택 > ML 알고리즘 fitting > backtest 해당 loop 반복
    • 동일한 데이터에 대해 지속적으로 테스트를 반복하는 것은 잘못된 발견으로 귀결 가능성 높음. 거짓 투자 전략을 발견하는데는 20번 정도의 반복이면 가능.

The Importance of Feature Importance

  • Backtest는 매우 쉽게 과적화 될 수 있다. 이 사실을 첫 번째로 인지해야 함
  • Cross Validation으로 모델을 fitting한 후 해당 모델의 metric을 보고 어떤 Feature에 의해서 모델의 metric이 좋은 성과를 냈는지를 판단해야 함. Feature에 대해 연구하고 Importance를 정량화 하여 최종적으로 선택하는 과정이 Research
  • 지도학습 모델들의 Feature를 연구하는 순간 black box가 아니게 된다. 머신러닝의 핵심은 학습하는 과정에서 지정해줘야 할 수많은 프로세스를 자동으로 처리한다는 점.
    • Lopez의 비유 : 사냥꾼들은 자신의 사냥개가 잡아온 모든 것을 먹지 않는다.
  • Research
    • Q1) 해당 Feature가 늘 중요한가? 특정 환경에서만 중요한가?
    • Q2) 시간에 따라 Feature의 중요성을 다르게 하는 것은 무엇인가?
    • Q3) 상황 변화(regime)을 예측할 수 있는가?
    • Q4) 중요하다고 결론낸 Feature가 다른 금융 상품에서도 중요한가?
    • Q5) 다른 자산 부류와도 관련이 있는가?
    • Q6) 모든 금융상품에 있어서 가장 공통적으로 관련있는 Feature는 무엇인가?

Feature Importance with Substitution Effects

  • Feature Importance를 해석할 때 주의해야 할 점은 Substitution Effects이다. 이는 통계에서 **다중공선성(Multi-collinearity)**과 철학적으론 동일하다.
  • 방법론들은 Substitution Effects에 영향을 받아서 실제로 받아야 할 중요성 보다 낮게 나타날 수 있다. 즉, 결합 효과를 고려한 Feature Importance 방법론이 있고 개별 단위로 Feature Importance를 계산하면 대체 효과에 관해선 영향 받지 않으나 결합 효과를 고려하지 못한다는 단점이 있다. 2가지 카테고리의 방법론을 Ensemble해서 쓰는 것이 좋음.
  • 위의 방법은 Quant Researcher들이 만들어낸 다양한 Feature 중에서 noise가 아닌 Feature Selection을 하는 방식이다. 여기서 PCA(Principal Component Analysis) 같은 방법을 사용하면 차원을 재조합하고 주요 특징으로만 Feature를 만들어내서 사용할 수도 있다. 다만, 이 방법은 선형 조합된 주요 특징들은 도메인이 반영되지 않은 수치 자체로만 최적화 되는 경향이 있기 때문에 약간의 위험성이 존재하고 PCA를 통해 나온 요약 결과를 검증하는 Tool로 사용하는 것도 하나의 방법이다.
  • Importance를 정량적으로 계산할 때 데이터를 어떻게 활용하느냐에 따라 In-sample(IS) 방식과 Out-of-sample(OS) 방식이 있는데 여러 용어가 혼용되는 경향이 있어 정리
    • Explanatory 방식 (In-Sample)
      • 설명(Explanation), Train data, 표본내 성능 검증(in-sample testing), explanatory-importance, explanatory-regression analysis
    • Predictve 방식 (Out-of-Sample)
      • 예측(Prediction), Test data, 표본외 성능 검증(out-of-sample testing), predictive-importance, predictive-regression analysis

MDI (Mean Decrease Impurity)

MDI에 대한 설명

image

  • image
    • 다른 이름으로는 Gini Importance 라고도 불린다. 위의 식은 Gini Impurity 라고 불리며 Decision-Tree 모델에서 각 노드의 Impurity를 낮추기 위해 최적화 작업이 진행된다. 위의 식을 직관적으로 해석하면 각 Class에 Sample이 골고루 분포되어 있을수록 Impurity는 높아지고 한 방향에 치우칠 수록 낮아진다. (순도, homogeneity 증가)
  • image
    • Tree 모델은 Node를 계속 타고 내려가는 구조이고 각 Node의 Importance도 계산할 수 있다. w_j는 전체 샘플 수에 대한 노드 C_j에 해당하는 샘플 수의 비율로 계산할 수 있고 j_left, j_right는 c_j 다음에서 갈라지는 두 node를 의미한다. node importanc 값이 클수록 해당 노드에서 Impurity가 크게 감소했다는 것을 의미한다.
  • image image
    • 각 feature의 계산은 Node Importance를 기반으로 중요도를 나타낼 수 있고, 2번째 식은 normalize한 것이다
    • Question) Split하는 Feature의 Order에 따라 Importance 계산 값이 달라질 것 같은데 DecisionTree model은 어떤 식으로 해결하는가?
      • 추측) 첫 node 마다 impurity를 가장 낮게 해주는 주요 feature부터 계산하는 식으로 ordering을 진행할 것으로 예상 -A) random seed의 존재에 따라 값이 다르게 나오는 걸 보면 random으로 ordering이 진행되는 것으로 보임

MDI 특징

  • Random Forest, Decision-Tree 모델에 특화되어 있어 Feature Importance 계산 시에 model dependency가 존재. (Tree 기반 아닌 모델에서 사용 X) Louppe, 2013
    • Model Specific
  • 특정 Feature에 Bias를 갖을 수 있어서 다른 특징을 무시하는 Mask Effect 발생할 수 있음. Storbl, 2007
  • 표본 내 성능 검증이므로 예측력이 없는 Feature도 중요도를 갖는 것처럼 나옴.
  • 중요도는 0~1의 값을 갖는 수학적으로 좋은 특성을 지님.
  • 대체효과에 의해서 동일한 두 가지 특징이 있는 경우 실질 중요도 보다 절반으로 감소됨.
def featImpMDI(fit, featNames):
    # feat importance based on IS mean impurity reduction
    df0 = {i: tree.feature_importances_ for i, tree in enumerate(fit.estimators_)}
    df0 = pd.DataFrame.from_dict(df0, orient='index')
    df0.columns = featNames
    df0 = df0.replace(0, np.nan) # because max_features = 1
    imp = pd.concat({'mean': df0.mean(), 'std': df0.std() * df0.shape[0] ** -0.5}, axis=1)
    imp /= imp['mean'].sum()
    return 

Mean Decrease Accuracy

MDA에 대한 설명

  • Permutation Feature Importance 라고도 불리며 기본적인 컨셉은 X1~X_n까지 n개의 feature가 존재하는 경우에 제대로 모든 n개의 feature를 train 했을 때의 metric performance와 n개 중의 1개의 feature를 shuffle하여 train 했을 때의 성능 손실 정도가 클 수록 중요한 feature로 판단한다. 그렇기에 model dependency가 없는 방법이다.

MDA 특징

  • 표본외 성능 검증(out-of-sample testing)이 진행됨.
  • 모든 Classifier에 적용 가능함
  • 성능 metric을 다양하게 사용 가능. ex) Accuracy, F1 Score, neg log loss
  • 상관된 특징이 있는 경우 importance 값에 영향을 미침
  • MDI와 달리 모든 특징이 중요하지 않는 다는 결론을 내릴 수 있음. why? 표본외 성능 검증이기 때문.
def featImpMDA(clf, X, y, cv, sample_weight, t1, pctEmbargo, scoring='neg_log_loss'):
    # feat importance based on OOS score reduction
    if scoring not in ['neg_log_loss', 'accuracy']:
        raise ValueError('wrong scoring method')
    from sklearn.metrics import log_loss, accuracy_score
    cvGen = PurgedKFold(n_splits=cv, t1=t1, pctEmbargo=pctEmbargo)
    scr0, scr1 = pd.Series(), pd.DataFrame(columns=X.columns)
    for i, (train, test) in enumerate(cvGen.split(X=X)):
        X0, y0, w0 = X.iloc[train, :], y.iloc[train], sample_weight.iloc[train]
        X1, y1, w1 = X.iloc[test, :], y.iloc[test], sample_weight.iloc[test]
        fit = clf.fit(X=X0, y=y0, sample_weight=w0.values)
        if scoring == 'neg_log_loss':
            prob = fit.predict_proba(X1)
            scr0.loc[i] = -log_loss(y1, prob, sample_weight=w1.values, labels=clf.classes_)
        else:
            pred = fit.predict(X1)
            scr0.loc[i] = accuracy_score(y1, pred, sample_weight=w1.values)
        
        for j in X.columns:
            X1_ = X1.copy(deep=True)
            np.random.shuffle(X1_[j].values) # permutation of a single column
            if scoring == 'neg_log_loss':
                prob = fit.predict_proba(X1_)
                scr1.loc[i, j] = -log_loss(y1, prob, sample_weight=w1.values, labels=clf.classes_)
            else:
                pred = fit.predict(X1_)
                scr1.loc[i, j] = accuracy_score(y1, pred, sample_weight=w1.values)
    
    imp = (-scr1).add(scr0, axis=0)
    if scoring == 'neg_log_loss':
        imp = imp / -scr1
    else:
        imp = imp / (1.0 - scr1)
    
    imp = pd.concat({'mean': imp.mean(), 'std': imp.std() * imp.shape[0] ** -0.5}, axis=1)
    return imp, scr0.mean()
  • 코드 분석
    • PurgedKFold, if scoring == 'neg_log_loss': 이 부분은 Cross-Validataion-in-Model 을 참고하면 왜 이렇게 짤 수 있는지 이해할 수 있다.
    • 핵심적으로 봐야할 부분은 scr0.loc[i] = -log_loss(y1, prob, sample_weight=w1.values, labels=clf.classes_) src0를 계산하는 부분 이 부분은 모든 feature를 제대로 넣고 계산하는 파트이고 neg_log_loss와 accuracy 모두 구현되어 있는 상태
    • np.random.shuffle(X1_[j].values) # permutation of a single column 을 통해서 j번째 column을 섞고 그대의 score를 src1에 저장함.
    • src0, src1을 가지고 importance를 imp = (-scr1).add(scr0, axis=0) 다음과 같이 계산함.

Feature Importance without Substitution Effects

  • Substitution Effects를 고려하지 않은 Feature Importance 방법론을 사용하게 되면 중요한 Feature를 중요하지 않은 걸로 나올 수 있다. 이를 위해서 보완할 방법이 필요하다. SFI(Single Feature Importance)를 활용해 보완할 수 있다.

Single Feature Importance

SFI 설명

  • Feature 하나씩 Performance를 측정하기 때문에 Cross-sectional 하다고 볼 수 있고, metric은 accuracy, neg log loss 무엇이든 상관없다.

SFI 특징

  • 모든 Classifier에 적용 가능하다.
  • metric으로 정량화된 모듈 아무거나 사용해도 가능하다.
  • out-of-sample testing 방식을 사용하기 때문에 모든 특징이 중요하지 않다는 결론을 내릴 수 있다.
def auxFeatImpSFI(featNames, clf, trnsX, cont, scoring, cvGen):
    imp = pd.DataFrame(columns=['mean', 'std'])
    for featName in featNames:
        df0 = cvScore(clf, X=trnsX[[featName]], y=cont['bin'], sample_weight=cont['w'], scoring=scoring, cvGen=cvGen)
        imp.loc[featName, 'mean'] = df0.mean()
        imp.loc[featName, 'std'] = df0.std() * df0.shape[0] ** -0.5
    return imp
  • 코드 분석
    • for featName in featNames:을 통해 single feature 마다 loop
    • single feature 마다 cvScore(clf, X=trnsX[[featName]], y=cont['bin'], sample_weight=cont['w'], scoring=scoring, cvGen=cvGen) metric 계산함.
    • 마지막엔 normalize를 함

Orthogonal Features

Orthogonal Features 설명

  • image image image
  • image image
  • image
  • image
    • PCA를 통해 나오는 새롭게 선형 조합된 Feature는 모든 Substitution Effects를 감소시키진 않지만 Linear Substitution Effects는 감소시킬 수 있다.
    • Feature Matrix X (t X n) 가 있고 이를 sigma_n (1 X n) vector와 mu_n (1 X n) vector로 표준화한 matrix가 Z (t X n)이다.
    • 고윳값 분해를 통해서 Lambda diagonal matrix (n x n, descending order)와 W orhtonormal matrix(n x n)를 구한다. Z`Z = n X n matrix
    • orthonormal feature matrix P = ZW로 계산하고 P`P의 계산을 통해 orthonormality를 검증한다.
    • Z를 계산하여 고윳값 분해를 진행하는 이유?
      • 데이터 중앙화 : 첫 번째 주성분이 Observations(X, Train data)의 주방향과 정확히 일치시켜 표현하게 됨.
      • 데이터 스케일링 : 분산 보다 상관관계를 설명하는데 더 집중. 만약 안하면 분산이 가장 큰 Feature에 주도 당하는 결과가 산출됨.
def get_eVec(dot, varThres):
    eVal, eVec = np.linalg.eigh(dot)
    idx = eVal.argsort()[::-1]
    eVal, eVec = eVal[idx], eVec[:, idx]
    eVal = pd.Series(eVal, index=['PC_'+str(i+1) for i in range(eVal.shape[0])])
    eVec = pd.DataFrame(eVec, index=dot.index, columns=eVal.index)
    eVec = eVec.loc[:,eVal.index]
    cumVar = eVal.cumsum() / eVal.sum()
    dim = cumVar.values.searchsorted(varThres)
    eVal, eVec = eVal.iloc[:dim+1], eVec.iloc[:,:dim+1]
    return eVal, eVec

def orthoFeats(dfX, varThres=0.95):
    dfZ = dfX.sub(dfX.mean(), axis=1).div(dfX.std(), axis=1)
    dot = pd.DataFrame(np.dot(dfZ.T, dfZ), index=dfX.columns, columns=dfX.columns)
    eVal, eVec = get_eVec(dot, varThres)
    dfP = np.dot(dfZ, eVec)
    return  dfP

def PCA_rank(dfX):
    dfZ = dfX.sub(dfX.mean(), axis=1).div(dfX.std(), axis=1)
    dot1 = np.nan_to_num(np.dot(dfZ.T, dfZ))
    eVal1, eVec1 = np.linalg.eig(dot1)

    perm = np.random.permutation(dfZ.columns)
    dfZ = dfZ.reindex(perm, axis=1)
    dot = np.nan_to_num(pd.DataFrame(np.dot(dfZ.T, dfZ), index=perm, columns=perm))
    eVal, eVec = np.linalg.eig(dot)

    return pd.Series(eVal.shape[0] - eVal.argsort().argsort(), index=dfX.columns, name='PCA_rank')
  • 코드 분석
    • get_eVec을 통해서 W matrix (eVec)를 계산한다. 함수 안의 내용은 수식을 코드화 했을 뿐.
    • orthoFeats을 통해 P matrix(dfP, orthonormal features)를 계산함.

Orthogonal Features 주요 특징 및 주의할 점

  • 직교화를 통해 고윳값과 연계된 정도가 작은 특징을 버림으로써 차원 축소와 연산 속도를 증가시킬 수 있고 직교 특징을 얻어낼 수 있음. 데이터 구조를 해석하는데 도움이 된다.
  • PCA는 기본적으로 label에 대한 지식 없이 (Unsupervised Learning)을 통해 어떤 특징이 다른 특징 보다 주요(Principal)하다는 결정을 내린다. 즉, 과적합의 가능성을 고려하지 않음
  • PCA 검증 활용 방법
    • 모든 feature가 random이라면 PCA의 순위와 MDI,MDA,SFI 순위와 일치하지 않을 것이다. 유사할 수록 과적합 가능성이 낮음을 의미한다고 해석 가능
    • egien values (inverse of pca rank) ~~ mdi,mda,sfi rank 의 weighted Kendall’s tau를 계산하여 과적합 상태를 체크할 수 있다.

Parallelized VS. Stacked Feature Importance

Parallelized

  • image
  • image image
  • image
    • small lambda : 각 상품 i, 기준(train data-label data) k에 대한 특징 중요도 j를 나타내고 이를 병합해 large lambda(j,k) 를 도출할 수 있고 투자 상품 영역 전반에 걸쳐 중요할 수록 기저 현상(theoritical mechanism) 일 가능성이 높다.
    • 병렬적으로 계산이 가능하고 각 상품 별로 특징 순위가 바뀔 수 있으나 이를 평균화하는 방식으로 사용할 수 있다. 다만, 상품에 대해 평균화 하는 방식은 결합효과를 일부 놓치게 된다.

Stacked

  • image

    • 서로 다른 상품을 하나의 dataset으로 stacking 하는 방식이고 이땐 X'는 standardized on a rolling trailing window 여야 한다.
      • X가 IID 하면 X'도 IID
    • Parallelized version 에 비해 훨씬 대규모 데이터셋에 적합화 되고, 중요도를 계산하는 과정은 간단해진다.
    • 이상 값과 과적합에 의한 편향이 적다.
    • 결합효과를 놓치지 않게 된다.
    • Stacking은 데이터가 많아질수록 엄청난 메모리와 자원을 소모한다. 그러므로 HPC(High Performance Computing) 이 매우 중요해짐
  • Feautre Importance를 계산할 때의 두 가지 방식

Application to Quant System

  • PCA feature 중 가장 주요한 feature는 feature로 추가해봐도 될 듯. 물론 redundancy가 발생하겠지만.
  • feature importance 계산엔 multi-processing을 붙여야 한다.
    • labeling case가 너무 다양함.
    • backtest로 바로 넘어가기 전에 model 단에서 많은 것을 끝내서 backtesting 쪽의 연산량을 최소한으로 줄여줘야 함
  • stacking 방식으로 접근하는게 좋을 것 같음.
Clone this wiki locally