작성자 : 투빅스 13기 이재빈
Contents
- Intro
- Shapley Value
- Additive Feature Attribution Method
- SHAP
- Code
Summary : SHAP
을 통해 Feature Attribution
을 파악할 수 있습니다.
좋은 집을 찾고 있는 두빅스씨 ...
어떤 집 하나가 유난히 가격이 낮은데, 그 집이 숲 속에 있기 때문인지, 평수가 작기 때문인지, 혹은 평수가 작아 고양이를 기를 수 없어서 그렇기 때문인지 정확한 이유를 알 수 없습니다.
결과만 보고 해석하지 않고, 각 요소들이 결과값에 얼마나 영향을 미치는 지에 대해 파악하고자 합니다.
기존의 Feature Attribution 파악 방법론에는 한계가 존재합니다.
Global vs Local 대리 분석
- Global Surrogate Analysis
학습 데이터(일부 또는 전체)를 사용해 대리 분석 모델을 구축하는 것- Local Surrogate Analysis
학습 데이터 하나를 해석하는 과정
Shapley Value
는 게임이론을 바탕으로, Game 에서 각 Player 의 기여분을 계산하는 방법입니다.
✔︎ Game Theory
개인 또는 기업이 어떠한 행위를 했을 때, 그 결과가 게임에서와 같이 자신뿐만 아니라 다른 참가자의 행동에 의해서도 결정되는 상황에서, 자신의 최대 이익에 부합하는 행동을 추구한다는 수학적 이론✔︎ 협력적 게임 이론 (Cooperative Game Theory)
비협력적으로 게임을 했을 때 각 개인이 취하는 이득보다, 협력적으로 게임을 했을 때의 각 개인이 취하는 이득이 더 크다면, 긍정적인 협동이 가장 최선의 선택지대표적인 예시 : 죄수의 딜레마
하나의 특성에 대한 중요도를 알기 위해 → 여러 특성들의 조합을 구성하고 → 해당 특성의 유무에 따른 평균적인 변화를 통해 값을 계산합니다.
집값을 결정짓는 요인으로, [숲세권, 면적, 층, 고양이 양육 가능 여부] 등의 Feature 가 존재합니다.
'고양이 양육 가능 여부' 의 집값에 대한 기여분은, 그 외 모든 Feature 가 동일하다는 가정 하에,
310,000 (고양이 함께 살기 불가능) - 320,000 (고양이 함께 사는 것 가능) = -10,000 으로 계산됩니다.
이러한 과정을 모든 가능한 조합에 대해 반복합니다.
[숲세권, 면적, 층, 고양이 양육 가능 여부] : 4개
= 8 개의 조합이 존재하며, 8개의 조합에 각각에 대해
{f(조합1 😺) - f(조합1)} + ... + {f(조합8 😺) - f(조합8)} 값을 산출하고, 이를 가중평균 하여 (😺) 를 구합니다.
Additive Feature Attribution Methods
have an explanation model that is a linear function of binary variables :
ex. LIME, DeepLIFT, Layer-Wise Relevance Propagation, Shapley Value
복잡한 모델 대신, 해석이 간단한 모델 로 해석하고자 합니다.
는 복잡한 모델에 특화되어 있는, 복잡한 데이터입니다.
따라서 의 해석 용이성을 위해 간단화 된 변수 를 사용하고자 하며,
Simplified input 는 라는 mapping function 으로 정의됩니다.
라는 가정을 통해, 으로 표현되며,
(1 : input is included in the model / 0 : excluded)
가 되도록 Surrogate Model 은 학습됩니다.
✔︎ Additive Feature Attrbution
원래의 input 가 아닌, Simplified input 를 통해,
를 만족하는 Explanation Model 를 만드는 방법입니다.
✔︎ Desirable Properties
Feature Attribution 은 1. Local Accuracy
, 2. Missingness
, 3. Consistency
특성 모두를 만족하기를 원합니다.
simplified input 를 explanation model 에 넣었을 때의 output 는, original input 에 대한 와 match 가 잘 되어야 합니다.
✓ (Efficiency) 각 팀원 점수를 합치면, 전체 점수가 되어야 합니다.
Feature 값이 없다면, Attribution 값도 0이 되어야 합니다.
✓ 팀플에 참여하지 않았다면, 개인 점수는 0점이 되어야 합니다.
모델이 변경되어 Feature 의 Marginal Contribution 이 (다른 특성에 관계없이) 증가하거나 동일하게 된다면, Attribution 도 증가하거나 동일하게 유지되어야 하며, 감소할 수 없습니다.
✓ 매번 똑같은 방식으로 play 했다면, 결과 점수가 똑같아야 합니다.
Additive Feature Attribution
의 정의와 3가지 Desired Properties
를 만족하는 유일한 방법은, Theorem1 수식입니다.
SHAP
= Shapley Value 를 사용하여, Additive Method 를 만족시키는 Explanation Model
SHAP
: Shapley Value 의 Conditional Expectation
Simplified Input을 정의하기 위해 정확한 값이 아닌, 의 Conditional Expectation을 계산합니다.
오른쪽 화살표 () 는 원점으로부터 가 높은 예측 결과를 낼 수 있게 도움을 주는 요소이고, 왼쪽 화살표 () 는 예측에 방해가 되는 요소입니다.
instance 에 대한 explanation 을 구합니다.
Tree Ensemble Model 의 Feature Importance 는 모델(or 트리)마다 산출값이 달라지기 때문에, Inconsistent
하다는 한계점이 존재합니다.
Tree Model A 와 B 의 Feature 는 Fever
, Cough
로 동일하지만, split 순서가 다릅니다. Split 순서에 기반하여 계산되는 Gain, Split Count 등의 산출값이 달라지는 것을 확인할 수 있습니다.
즉, 같은 데이터로부터 학습이 된 모델이지만, 모델마다 Feature Importance 가 달라지게 됩니다.
SHAP 은 Split 순서와 무관하게, Consistent 한 Feature Importance 를 계산할 수 있습니다.
= 가 속하는 Leaf node의 score 값 해당 node에 속하는 Training 데이터의 비중
각 Tree 마다 Conditional Expectation 를 구해, 다 더하여 최종 값을 산출합니다.
시간복잡도 :
T : 트리의 개수, L : 모든 트리의 최대 leaf 수, D : 모든 트리의 최대 깊이
Kernel SHAP 에 비해 속도가 빠릅니다.
# import packages
import pandas as pd
import numpy as np
from xgboost import XGBRegressor, plot_importance
from sklearn.model_selection import train_test_split
import shap
# load data
X, y = shap.datasets.boston()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.2, random_state=1)
# modeling
model = XGBRegressor()
model.fit(X_train, y_train)
보스턴 주택 가격 데이터를 통해, 집값을 결정짓는 요인에 대해 알아봅니다.
XGBoost Regressor
로 집값을 예측하는 모델을 만들고, SHAP value
를 통해 Feature Attribution 을 파악합니다.
# load js
shap.initjs()
'''
KernelExplainer() : KNN, SVM, RandomForest, GBM, H2O
TreeExplainer() : tree-based machine learning model (faster)
DeepExplainer() : deep learning model
'''
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_train)
특정 데이터 하나 & 전체 데이터에 대해, Shapley Value 를 1차원 평면에 정렬해서 보여줍니다.
# 첫 번째 데이터에 대한 SHAP 시각화
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[0,:], X_train.iloc[0,:])
특정 데이터 (궁금한 어떤 주택)에 대한 Shapley value 를 분해하고 시각화합니다.
집값 상승에 긍정적인 영향을 준 요인은 LSTAT (동네의 하위 계층 비율) 이며, 부정적인 영향을 준 요인은 RM (방의 수) 입니다.
비슷한 조건의 다른 주택에 비해 방의 수 요인이 집값 형성에 부정적인 영향을 미쳤다고 해석할 수 있습니다.
# Outlier 에 대한 SHAP 시각화
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[259,:], X_train.iloc[259,:])
주요 업무지까지 거리가 가깝고 (DIS), 주변 지역에 하위 계층 사람들이 적게 살면서 (LSTAT), 범죄율이 극도로 낮으므로 (CRIM), 주택 자체의 상태보다 주변 환경에 의해 좋은 집값이 형성되었음을 알 수 있습니다.
# 전체 데이터에 대한 SHAP 시각화
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values, X_train)
전체 데이터에 대해 Shapley Value를 누적하여 시각화 할 수 있으며, 이를 기반으로 Supervised Clustering
또한 진행할 수 있다고 합니다.
Feature 하나에 대한 전체 누적 Shapley Value 를 구할 수 있습니다.
Feature 하나에 대한 SHAP 영향력을 보여줍니다.
shap.initjs()
top_inds = np.argsort(-np.sum(np.abs(shap_values), 0)) # (13, ) : 각각의 Feature 에 대해 shap value 다 더한 것
# make SHAP plots of the three most important features
for i in range(2):
shap.dependence_plot(top_inds[i], shap_values, X_train)
위의 코드는 영향력이 큰 변수들 순서대로 출력하는 방법이며, top_inds[i]
에 특정 Feature 를 넣어 영향력을 파악할 수 있습니다.
shap.summary_plot(shap_values, X_train)
전체 Feature 들이 Shapley Value 분포에 어떤 영향을 미치는지 시각화 할 수 있습니다.
shap.summary_plot(shap_values, X_train, plot_type='bar')
각 Feature 가 모델에 미치는 절대 영향도를 파악할 수 있습니다.
shap_interaction_values = shap.TreeExplainer(model).shap_interaction_values(X_train)
# main effect on the diagonal
# interact effect off the diagonal
shap.summary_plot(shap_interaction_values, X_train)
Feature 사이의 interaction 또한 파악할 수 있습니다.
shap.dependence_plot(
("RM", "LSTAT"),
shap_interaction_values, X_train,
display_features=X_train
)
dependency_plot
을 통해 두 개 변수 사이의 영향력 시각화 가능합니다.
영향력 분해 또한 가능하다고 합니다.
(출처 : Explainable AI for Trees: From Local Explanations to Global Understanding)
import transformers
transformer = transformers.pipeline('sentiment-analysis', return_all_scores=True)
explainer2 = shap.Explainer(transformer)
shap_values2 = explainer2(["What a great movie! ...if you have no taste."])
shap.plots.text(shap_values2[0, :, "POSITIVE"])
import tensorflow as tf
# 1. dataset 불러오기
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0 # image scaling : (0,1) 범위로 바꿔주기
# 2. model 구성하기
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10, activation='softmax')
])
# 3. model 학습과정 설정하기
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 4. model 학습
model.fit(x_train, y_train, epochs=3)
# select a set of background examples to take an expectation over
background = x_train[np.random.choice(x_train.shape[0], 100, replace=False)]
# explain predictions of the model on four images
e = shap.DeepExplainer(model, background)
# ...or pass tensors directly
# e = shap.DeepExplainer((model.layers[0].input, model.layers[-1].output), background)
shap_values = e.shap_values(x_test[:5])
# plot the feature attributions
shap.image_plot(shap_values, -x_test[:5])
mnist classification 에 대한 SHAP 시각화 결과입니다.
4
와 9
를 예측함에 있어서 -
부분이 핵심적인 역할을 한다고 해석할 수 있습니다.
from keras.applications.vgg16 import VGG16
from keras.applications.vgg16 import preprocess_input
import tensorflow.python.keras.backend as K
import json
# load pre-trained model and choose two images to explain
model = VGG16(weights='imagenet', include_top=True)
X,y = shap.datasets.imagenet50()
to_explain = X[[39,41]]
# load the ImageNet class names
url = "https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json"
fname = shap.datasets.cache(url)
with open(fname) as f:
class_names = json.load(f)
# explain how the input to the 7th layer of the model explains the top two classes
def map2layer(x, layer):
feed_dict = dict(zip([model.layers[0].input], [preprocess_input(x.copy())]))
return K.get_session().run(model.layers[layer].input, feed_dict)
e = shap.GradientExplainer(
(model.layers[7].input, model.layers[-1].output),
map2layer(X, 7),
local_smoothing=0 # std dev of smoothing noise
)
shap_values,indexes = e.shap_values(map2layer(to_explain, 7), ranked_outputs=2)
# get the names for the classes
index_names = np.vectorize(lambda x: class_names[str(x)][1])(indexes)
# plot the explanations
shap.image_plot(shap_values, to_explain, index_names)
각 Class 를 예측함에 있어서, 어떤 부분이 중요한 역할을 했는지 볼 수 있습니다.
SHAP
: https://github.com/slundberg/shap