Permutation importance 을 사용하여 딥러닝 모델 해석하기 (정형 데이터)

2020. 3. 26. 22:54관심있는 주제/XAI

광고 한번만 눌러주세요 ㅎㅎ 블로그 운영에 큰 힘이 됩니다.

모델을 해석하는 방법에는 다양한 방법이 있고, 그중에 많은 패키지는 model-agnostic 한 방법으로 학습된 모델을 사후 해석하는 방향으로 해석을 합니다. 

그래서 이번 글에서는 그러한 방법론 중 Permutation을 이용하여 변수 중요도를 구하는 방법을 소개하겠습니다.

여기서 사용하고 있는 많이들 알고 있는 Tensorflow를 사용하여 Neural Network를 기반 아키텍처에 대한 변수에 대한 중요도를 보이고자 합니다.

아래 그림 처름 주어진 데이터에 각 변수마다 Permutation을 통해 다양하게 섞습니다.

그리고 Permutation을 하였을 때, 결괏값의 Loss가 커질 경우, 그 변수는 중요한 변수라는 것을 의미합니다.

 

전처리가 된 데이터

일단 데이터를 전처리를 하고, 학습을 시켜보겠습니다.

모형은 기존에 만들어 놓은 모형이 있어서 그대로 사용하였습니다.

아키텍처는 다음과 같이 간단하게 구성하였습니다.

import tensorflow as tf
tf.reset_default_graph()
X = tf.placeholder(tf.float32 , shape = [None,train_x.shape[1]])
y = tf.placeholder(tf.float32 , shape = [None,1])
batch_size = tf.placeholder(tf.int64)
##
data_tuple = (X,y)
dataset = tf.data.Dataset.from_tensor_slices(data_tuple)
dataset = dataset.shuffle(buffer_size= 1000)
dataset = dataset.batch(batch_size=batch_size, drop_remainder=True)
##
iter = dataset.make_initializable_iterator()
feature_batch , label_batch = iter.get_next()
## 
W1 = tf.get_variable("w",shape=[train_x.shape[1],25])
B1 = tf.get_variable("b",shape=[25])
W2 = tf.get_variable("w2",shape=[25,15])
B2 = tf.get_variable("b2",shape=[15])
W3 = tf.get_variable("w3",shape=[15,1])
B3 = tf.get_variable("b3",shape=[1])
## 
def Layer(X , reuse = False) :
    layer1 = tf.nn.relu(tf.matmul(X, W1) + B1)
    layer2 = tf.nn.selu(tf.matmul(layer1, W2) + B2)
    logit = tf.matmul(layer2, W3) + B3
    return logit
## 
logit = Layer(feature_batch)
loss = tf.nn.sigmoid_cross_entropy_with_logits(labels = label_batch , logits=logit)
Loss = tf.reduce_mean(loss)
vars = tf.trainable_variables()
optimizer = tf.train.AdadeltaOptimizer(1e-3).minimize(Loss , var_list = vars)
prob= tf.nn.sigmoid(logit)
auc_value , update_auc = tf.metrics.auc(label_batch , prob , curve="ROC")
output = tf.nn.sigmoid(Layer(X))
tf.add_to_collection('Probability', X)
tf.add_to_collection('Probability', output)

>> 중간에 학습이 너무 안되서 고민을 하다가 찾아보니 $AdadeltaOptimizer$가 문제였다.

$$ \text{그래서} AdadeltaOptimizer \text{ -> } AdamOptimizer$$ 

바로 성능 향상 갓 $Adam$

학습 코드는 다음과 같습니다.

config = tf.ConfigProto(log_device_placement=True)
config.gpu_options.allow_growth = True
EPOCHS = 1000
with tf.Session(config=config) as sess:
    saver = tf.train.Saver()
    init = tf.group(tf.global_variables_initializer(), tf.local_variables_initializer())
    sess.run(init)
    for i in range(EPOCHS):
        
        sess.run(iter.initializer , 
                 feed_dict= {X : train_x.values,
                             y : train_y.values.reshape(-1,1),
                             batch_size : 1000}) # switch to train dataset
        while True :
            try :
                probs , _,auc,Loss2,_=sess.run([prob, auc_value,update_auc,Loss,optimizer])
            except tf.errors.OutOfRangeError :
                break
        sess.run(iter.initializer ,
                 feed_dict= {X : test_x.values,
                             y : test_y.values.reshape(-1,1),
                             batch_size : len(test_x)
                            }) # switch to train dataset
        probs , _ , auc = sess.run([prob,auc_value,update_auc])
        print(f"Epoch : {i}, AUC : {auc*100:.2f}", end="\r")
    meta_graph_bool = True
    saver.save(sess,
               "./tf_data_test_model.ckpt",
               global_step=i, write_meta_graph=meta_graph_bool)
############################################################################               
    sess.run(iter.initializer ,
             feed_dict= {X : train_x.values,
                         y : train_y.values.reshape(-1,1),
                         batch_size : len(train_x)
                        }) # switch to train dataset
    original_prob , original_score = np.squeeze(sess.run([prob,Loss]))
    final_score = []
    shuff_pred = []
    for i , indexs in enumerate(pipe[-1].trans_info["onehot_store"]) :
        first , to = indexs
        diff = to - first
        shuff_test = deepcopy(train_x.values)
        if diff == 1 :
            shuff_test[:,first] =\
            np.random.permutation(shuff_test[:,first])
        else :
            shuff_test[:,first:(first+diff)] =\
            np.random.permutation(shuff_test[:,first:(first+diff)])
        sess.run(iter.initializer ,
                 feed_dict= {X : shuff_test,
                             y : train_y.values.reshape(-1,1),
                             batch_size : len(shuff_test)}) # switch to train dataset
        probs , score = sess.run([prob,Loss])
        final_score.append(np.squeeze(score))
        shuff_pred.append(np.squeeze(probs))
##########################################################################
        

중간에 ### 이 부분에서 Permutation을 통해 모델 결괏값을 산출하는 부분입니다.

그다음에 산출된 값을 이용해서 다음과 같이 만듭니다.(임의로 만들다 보니 정말 정확하지 않을 수 있습니다.)

변수 상대적 중요도 = $\frac{|R-P|}{\sum(|R-P|)}*100$

$R$ : 실제 결과 $P$ : Permutation 결과

abs_value = np.abs(final_score-original_score)
FeatureImportance = abs_value / np.sum(abs_value) * 100
plt.subplots(figsize=(15,10))
idx = np.argsort(FeatureImportance)
plt.barh(range(len(totalvar)), FeatureImportance[idx])
plt.yticks(range(len(totalvar)),np.array(totalvar)[idx] ,fontsize= 20)
plt.show()

변수 중요도 시각화 코드!

columns = X_train.columns.tolist()
def plot_feature_importances_cancer(model,columns):
    plt.subplots(figsize=(15,10))
    n_features = len(columns)
    idx = np.argsort(model.feature_importances_)
    plt.barh(range(n_features), model.feature_importances_[idx], align='center')
    plt.yticks(np.arange(n_features), np.array(columns)[idx])
    plt.xlabel("특성 중요도")
    plt.ylabel("특성")

 

실제 퍼포먼스가 좋지 않은 상태에서 진행해서 결과에 대한 신용은 할 수 없지만, 
EDUCATION이라는 변수가 상대적으로 중요하다고 나옵니다.


CAUSATION RELATIONSHIPS

인과관계를 증명하기 위해 많은 방법론이 있다고 합니다.

유명한 것 중에 하나가  Granger Causality Test라고 합니다. 이 기술에 대해서는 잘 모르지만 시계열에서 자주 사용하는 것 같습니다.

인과관계를 증명하기 위해서, 해야 하는 것은 데이터 셔플이 성능 변화에 의미 있는 증거를 제공한다는 것을 증명하는 것입니다. 

np.random.seed(33)
random.seed(33)
id_ = 0
merge_pred = np.hstack([shuff_pred[id_], np.squeeze(original_prob)])
observed_diff = abs(shuff_pred[id_].mean() - merge_pred.mean())
extreme_values = []
sample_d = []
for _ in range(10000):
    sample_mean = np.random.choice(merge_pred, size=shuff_pred[id_].shape[0]).mean()
    sample_diff = abs(sample_mean - merge_pred.mean())
    sample_d.append(sample_diff)
    extreme_values.append(sample_diff >= observed_diff)
y, x, _ = plt.hist(sample_d, alpha=0.6)
plt.vlines(observed_diff, 0,max(y), colors='red', linestyles='dashed')
plt.title(f"{totalvar[id_]} p-value : {np.sum(extreme_values)/10000}",fontsize=10)
plt.show()

그래서 파란색 막대는 시뮬레이션된 평균의 차이를 나타내는 분포를 나타내고, 실제 관측된 차이는 빨간색으로 표시했습니다. LIMIT_BAL 같은 경우 셔플 없이 이루어진 예측과 함께 평균의 차이가 없다고 말할 수가 없습니다.
즉 이 변수는 인과관계를 증명할 수 없는 변수가 된다는 의미로 해석할 수 있을 것 같다.

$$H_0 : \text{셔플 없이 계산한 평균 차와 셔플 후 계산한 평균 차의 차이가 없다.}$$

$$H_1 : \text{셔플 없이 계산한 평균차와 셔플 후 계산한 평균차의 차이가 있다.}$$

728x90