-
Notifications
You must be signed in to change notification settings - Fork 2
Feature Importance
jaeaehkim edited this page Jun 9, 2022
·
7 revisions
- 잘못된 절차 : 특정 Data 선택 > ML 알고리즘 fitting > backtest 해당 loop 반복
- 동일한 데이터에 대해 지속적으로 테스트를 반복하는 것은 잘못된 발견으로 귀결 가능성 높음. 거짓 투자 전략을 발견하는데는 20번 정도의 반복이면 가능.
- 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를 해석할 때 주의해야 할 점은 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
- Explanatory 방식 (In-Sample)
-
- 다른 이름으로는 Gini Importance 라고도 불린다. 위의 식은 Gini Impurity 라고 불리며 Decision-Tree 모델에서 각 노드의 Impurity를 낮추기 위해 최적화 작업이 진행된다. 위의 식을 직관적으로 해석하면 각 Class에 Sample이 골고루 분포되어 있을수록 Impurity는 높아지고 한 방향에 치우칠 수록 낮아진다. (순도, homogeneity 증가)
-
- Tree 모델은 Node를 계속 타고 내려가는 구조이고 각 Node의 Importance도 계산할 수 있다. w_j는 전체 샘플 수에 대한 노드 C_j에 해당하는 샘플 수의 비율로 계산할 수 있고 j_left, j_right는 c_j 다음에서 갈라지는 두 node를 의미한다. node importanc 값이 클수록 해당 노드에서 Impurity가 크게 감소했다는 것을 의미한다.
-
- 각 feature의 계산은 Node Importance를 기반으로 중요도를 나타낼 수 있고, 2번째 식은 normalize한 것이다
- Question) Split하는 Feature의 Order에 따라 Importance 계산 값이 달라질 것 같은데 DecisionTree model은 어떤 식으로 해결하는가?
- 추측) 첫 node 마다 impurity를 가장 낮게 해주는 주요 feature부터 계산하는 식으로 ordering을 진행할 것으로 예상 -A) random seed의 존재에 따라 값이 다르게 나오는 걸 보면 random으로 ordering이 진행되는 것으로 보임
- 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
- Permutation Feature Importance 라고도 불리며 기본적인 컨셉은 X1~X_n까지 n개의 feature가 존재하는 경우에 제대로 모든 n개의 feature를 train 했을 때의 metric performance와 n개 중의 1개의 feature를 shuffle하여 train 했을 때의 성능 손실 정도가 클 수록 중요한 feature로 판단한다. 그렇기에 model dependency가 없는 방법이다.
- 표본외 성능 검증(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)
다음과 같이 계산함.
- PurgedKFold,
- Substitution Effects를 고려하지 않은 Feature Importance 방법론을 사용하게 되면 중요한 Feature를 중요하지 않은 걸로 나올 수 있다. 이를 위해서 보완할 방법이 필요하다. SFI(Single Feature Importance)를 활용해 보완할 수 있다.
- Feature 하나씩 Performance를 측정하기 때문에 Cross-sectional 하다고 볼 수 있고, metric은 accuracy, neg log loss 무엇이든 상관없다.
- 모든 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를 함
-
-
- 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)를 계산함.
-
- 직교화를 통해 고윳값과 연계된 정도가 작은 특징을 버림으로써 차원 축소와 연산 속도를 증가시킬 수 있고 직교 특징을 얻어낼 수 있음. 데이터 구조를 해석하는데 도움이 된다.
- 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를 계산하여 과적합 상태를 체크할 수 있다.
-
- small lambda : 각 상품 i, 기준(train data-label data) k에 대한 특징 중요도 j를 나타내고 이를 병합해 large lambda(j,k) 를 도출할 수 있고 투자 상품 영역 전반에 걸쳐 중요할 수록 기저 현상(theoritical mechanism) 일 가능성이 높다.
- 병렬적으로 계산이 가능하고 각 상품 별로 특징 순위가 바뀔 수 있으나 이를 평균화하는 방식으로 사용할 수 있다. 다만, 상품에 대해 평균화 하는 방식은 결합효과를 일부 놓치게 된다.
-
- 서로 다른 상품을 하나의 dataset으로 stacking 하는 방식이고 이땐 X'는 standardized on a rolling trailing window 여야 한다.
- X가 IID 하면 X'도 IID
- Parallelized version 에 비해 훨씬 대규모 데이터셋에 적합화 되고, 중요도를 계산하는 과정은 간단해진다.
- 이상 값과 과적합에 의한 편향이 적다.
- 결합효과를 놓치지 않게 된다.
- Stacking은 데이터가 많아질수록 엄청난 메모리와 자원을 소모한다. 그러므로 HPC(High Performance Computing) 이 매우 중요해짐
- 서로 다른 상품을 하나의 dataset으로 stacking 하는 방식이고 이땐 X'는 standardized on a rolling trailing window 여야 한다.
-
Feautre Importance를 계산할 때의 두 가지 방식
- PCA feature 중 가장 주요한 feature는 feature로 추가해봐도 될 듯. 물론 redundancy가 발생하겠지만.
- feature importance 계산엔 multi-processing을 붙여야 한다.
- labeling case가 너무 다양함.
- backtest로 바로 넘어가기 전에 model 단에서 많은 것을 끝내서 backtesting 쪽의 연산량을 최소한으로 줄여줘야 함
- stacking 방식으로 접근하는게 좋을 것 같음.