Permutation feature importance는 특성과 실제 결과값 간의 관계(연결고리)를 끊어내도록 특성들의 값을 랜덤하게 섞은 후 모델 예측치의 오류 증가량을 측정합니다. 만약 하나의 특성을 무작위로 섞었을 때 모델 오류가 증가한다면 모델이 예측할 때 해당 특성에 의존한다는 것을 의미하기 때문에 "중요한" 특성이라 할 수 있습니다. 반대로 오류에 차이가 없다면 그 특성은 "중요하지 않은" 특성이라고 할 수 있겠죠.
저는 permutation 과정을 "무작위로 섞는다"고 표현했는데, 다른 곳에선 이를 "각 특성을 제거하고 제거된 특성을 랜덤한 노이즈로 대체한다" 고 표현하기도 합니다.
무작위로 섞어 해당 특성과 결과값 간의 관계를 없앤다는 말과 동일하다는 점에서 어떤 표현을 쓰든 맥락은 같습니다.
Fisher, Rudin, Dominici의 논문에 따르면 permutation feature importance 알고리즘은 아래와 같습니다.
1. 원본 모델의 오류를 측정한다.
$e^{orig} = L(y, f(X))$ (e.g. MSE)
2. 각 특성 $j=1, ..., p$에 대해 아래 과정을 수행한다.
- 데이터 X에서 특성 $j$를 섞어가며 특성 행렬 $X^{perm}$를 생성한다. 이는 특성 $j$와 실제 결과값 $y$ 간의 관계를 끊는다.
- 섞여진 데이터의 예측치를 기반으로 오류를 측정한다.
$e^{perm} = L(Y, f(X^{perm})$
- permutation feature importance를 계산한다. 아래 2가지 계산법 모두 사용할 수 있다.
$FI^{j} = e^{perm} / e^{orig}$
$FI^{j} = e^{perm} - e^{orig}$
3. FI 값에 따라 특성을 역순으로 정렬한다.
추가로 논문에선 계산 과정을 실제 특성을 무작위로 섞는(permutation) 방법 대신 계산 복잡도를 줄이기 위한 대안으로 다른 방법을 제안합니다. 데이터셋을 반으로 쪼개고 특성 $j$값을 2개의 나뉘어진 데이터셋에서 서로 교환합니다. 이는 특성 $j$를 무작위로 섞는 것과 동일한 효과를 가져옵니다.
Permutation feature importance를 소개하고 있는 Interpretable Machine Learning에 흥미로운 주제가 있었습니다. 바로 "학습셋을 바탕으로 중요도를 계산해야 하는지, 테스트셋을 바탕으로 계산해야 하는지" 였는데요, 같이 살펴보도록 하시죠.
결론은 명확한 답이 있는 것이 아니라 스스로 판단해야 한다고 합니다 :)
학습셋을 바탕으로 한 특성 중요도와 테스트셋을 바탕으로 한 특성 중요도 간의 차이점을 이해하는 가장 좋은 방법은 "극단의" 샘플을 보는 것입니다. 아래의 상황을 예로 들어보겠습니다.
모델: SVM
데이터 개수: 200
특성 개수: 50
타겟: 연속적인 랜덤값(continous random target)
위에서 "랜덤"이라고 표현한 것은 타겟이 50개 특성과 독립적이라는 것을 말합니다. 이는 마치 지난 로또 번호를 가지고 내일의 온도를 맞추는 것과 같습니다. 만약 모델이 어떠한 관계라도 "학습한다"면 이는 오버피팅된 것입니다. 그리고 실제로 SVM은 학습 데이터에 오버피팅됩니다. MAE(mean absolute error)는 학습셋에 대해 0.29, 테스트셋에 대해 0.82를 기록했습니다. 쉽게 말해 SVM은 아무 쓸모가 없는 모델이 된 것입니다.
이렇게 오버피팅된 SVM의 50개 특성 중 어떤 특성이 진정으로 중요하다고 할 수 있을까요?
"테스트셋의 성능에 기여한 특성이 없기 때문에 모두 중요하지 않다." vs "학습 결과가 테스트셋에 대해 일반화가 가능한지 여부와 상관없이 학습 시 모델이 얼마나 각 특성에 의존적인지를 반영해야 한다."
학습셋과 테스트셋의 특성 중요도가 어떻게 다른지 아래 그림을 봅시다.
학습셋, 테스트셋 각 경우에 맞는 예시를 들어보겠습니다.
학습셋 케이스
학습셋을 기반으로 했을 때 중요한 특성은 X42 입니다. 아래 그래프는 X42의 PDP(partial dependence plot)로, 모델의 결과값이 특성의 변화에 따라 얼마나 달라지는지를 보여줍니다. 일반화(generalization) 오류와는 상관이 없는데, 이는 다시 말해 PDP가 학습셋 위에서 계산되었든, 테스트셋 위에서 계산되었든 상관이 없다는 것입니다.
하지만 테스트셋의 특성 중요도를 살펴보면 X42는 결코 중요한 특성이 아닙니다. 학습셋에 기초한 특성 중요도는 모델이 예측을 하는데 어느 특성에 "의존했는가"를 말해줍니다.
실질적으로는 마지막에 모델을 훈련할 때 결국 모든 데이터를 사용할 것입니다. 이는 곧 특성 중요도를 계산할 때 더 이상 남아있는 테스트셋(=unseen data)이 없음을 의미합니다. 특성 중요도를 측정하기 위해 교차검증(cross-validation)을 할 때 역시 마찬가지입니다. 교차검증이기 때문에 매번 모든 데이터를 사용하지 못하게 되는데, 빠지게 되는 일부분의 데이터가 모델에 큰 영향을 줄 수도 있는 것입니다.
정리하자면 각 데이터셋에 따른 특성 중요도는 다음과 같은 의미를 지닙니다.
학습셋
- 모델이 예측치를 만드는 데 얼마나 각 특성에 의존했는지
테스트셋
- 학습하지 않은 데이터셋에 대한 모델 성능에 각 특성이 얼마나 기여했는지
장점
1. 다른 특성과의 모든 상호작용 고려
- 특성을 섞으면 다른 특성과 상호작용 효과도 파괴됩니다. 즉 모델 성능에 대한 주요 특성 효과와 상호작용 효과를 모두 고려하는 것이죠. 하지만 이는 동시에 단점이 되기도 하는데, 두 특성 간 상호작용 중요도가 두 특성 중요도 측정에 포함되기 때문입니다. 다시 말하면, 특성 중요도가 전체 성능 저하에는 미치지 못하지만 합계는 더 커지는 것입니다. 특성 간 상호작용이 없을 때에만 중요도가 전체 성능에 근사할 수 있습니다.
2. 모델을 재학습할 필요 X
- 몇몇 방법들은 하나의 특성을 지우고 모델을 재학습시켜 오류를 비교합니다. 하지만 재학습은 오랜 시간이 걸리기 때문에 "오직" 특성을 섞는 것만이 시간을 아낄 수 있습니다. 또한, 줄어든 데이터셋을 학습하면 우리가 관심있어 하던 것과는 다른 모델을 만들어낼 위험이 있습니다.
단점
1. 학습셋이냐, 테스트셋이냐 무엇을 사용해야 하는지 불명확함
2. 모델의 오류와 연결되는 가능성 존재
- 모델의 오류를 신경쓰는 것은 어찌보면 당연하지만, 어떤 경우에는 꼭 필요하지 않습니다. 성능에 대한 의미보단 모델의 결과값이 특성에 의해 얼마나 달라지는지만 알고 싶을 수 있죠. 즉 특성을 섞을 때 모델 성능 감소 정도에는 관심이 없지만 모델의 결과값 분산이 어느정도 설명되는지 관심이 있는 것입니다. 일반적으로 모델의 분산(특성에 의해 설명)과 특성 중요도는 모델이 잘 일반화할 때(=과적합되지 않음) 강한 상관관계를 보입니다.
3. 비현실적인 데이터에 의해 편향될 가능성 존재
- 2개 이상의 특성들 간 상관관계를 보인다면 특성을 섞은 결과가 비현실적인 데이터를 생성해내는 문제가 있습니다(이는 PDP에서도 나타나는 문제).
4. 결과의 불안정성
- 특성을 랜덤하게 섞기 때문에 과정을 진행할 때마다 결과값이 상이해질 수 있습니다. 물론, 여러 번 실행하여 평균값을 구해도 되지만 계산 소요 시간이 많이 듭니다.
지금까지 설명한 Permutation Feature Importance에 대한 파이썬 실습 코드를 간단하게 살펴보겠습니다.
from sklearn.datasets import load_breast_cancer # 암 여부 판별 데이터
cancer = load_breast_cancer() # dictionary 형태
cancer.keys()
data = np.c_[cancer['data'], cancer['target']] # array([['data', 'target'], [] ...])
columns = np.append(cancer['feature_names'], ['target'])
df = pd.DataFrame(data, columns=columns)
X = df[df.columns[:-1]]
y = df['target']
print(X.shape)
print(y.shape)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)
print('학습셋 형태는: ', X_train.shape)
print('테스트셋 형태는: ', X_test.shape)
# mean area 및 mean concave points 컬럼 대표로 살펴보기
sns.set_style("whitegrid")
plotOne = sns.FacetGrid(df, hue="target", aspect=2.5)
plotOne.map(sns.kdeplot, 'mean area', shade=True)
plotOne.set(xlim=(0, df['mean area'].max()))
plotOne.add_legend()
plotOne.set_axis_labels('mean area', 'Proportion')
plotOne.fig.suptitle('Area vs Diagnosis (Blue = Malignant; Orange = Benign)')
plt.show()
sns.set_style("whitegrid")
plotTwo = sns.FacetGrid(df, hue="target", aspect=2.5)
plotTwo.map(sns.kdeplot, 'mean concave points', shade= True)
plotTwo.set(xlim=(0, df['mean concave points'].max()))
plotTwo.add_legend()
plotTwo.set_axis_labels('mean concave points', 'Proportion')
plotTwo.fig.suptitle('# of Concave Points vs Diagnosis (Blue = Malignant; Orange = Benign)')
plt.show()
모델은 랜덤포레스트 분류기를 사용하겠습니다.
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=100, random_state=0).fit(X_train, y_train)
먼저 랜덤포레스트에서 제공하는 불순도를 기준으로 하는 변수 중요도를 구해보겠습니다.
columns = X_train.columns
coefficients = model.feature_importances_.reshape(X_train.columns.shape[0], 1)
pd.concat([pd.DataFrame(columns, columns=['변수명']), pd.DataFrame(coefficients, columns=['특성 중요도'])], axis=1).sort_values(by='특성 중요도', ascending=False)
Permutation Feature Importance 모듈은 eli5라는 라이브러리에서 제공합니다. 가중치를 제공하는 기능 외에도 이로부터 모델을 선택하는 기능도 있으니 공식 홈페이지를 참고하면 되겠습니다.
# !pip install eli5
import eli5
from eli5.sklearn import PermutationImportance
perm = PermutationImportance(model, random_state=1).fit(X_test, y_test)
eli5.show_weights(perm, feature_names=X_test.columns.tolist())
트리 기반 모델에서 제공하는 불순도 기반의 변수 중요도와 Permutation Feature Importance 결과가 상당히 다름을 알 수 있습니다. 물론, 데이터셋에 따라 다르겠지만 이와 같은 결과는 유의미하다고 할 수 있겠습니다. 다만, 어떻게 해석해야 하는지는 순수 데이터 분석 결과에 의존할 것이 아니라 도메인 지식과도 연결하는 유연한 자세가 필요할 것입니다.
참조
https://christophm.github.io/interpretable-ml-book/feature-importance.html
https://www.kaggle.com/dansbecker/permutation-importance
'문돌이 존버 > 데이터 분석' 카테고리의 다른 글
시퀀스-투-시퀀스(sequence-to-sequence) 간단히 이해하기 (0) | 2021.08.16 |
---|---|
(Explainable AI) Partial Dependence Plot 개념 이해하기 (0) | 2021.08.09 |
(Explainable AI) LIME 실습 코드 돌려보기 (0) | 2021.08.04 |
(Explainable AI) LIME 개념 이해하기 (2) | 2021.08.02 |
(Explainable AI) SHAP 그래프 해석하기! feat. 실전 코드 (0) | 2021.07.30 |