오늘부터 머신러닝, 딥러닝 파트가 다시 시작되었다. XAI라는 설명 가능한 인공지능에 대해 배웠는데, 이전에 프로젝트를 준비하면서 관심을 가졌던 내용이라 상당히 흥미롭게 수업을 들었다.
df.head()

df.describe().round(3)

df['Geography'].value_counts(dropna=False).sort_index()
# Geography
# France 9053
# Germany 2663
# Spain 3283
# NaN 1
# Name: count, dtype: int64
df['Gender'].value_counts(dropna=False).sort_index()
# Gender
# Female 6545
# Male 8455
# Name: count, dtype: int64
crosstab 으로 함께 조회pd.crosstab(index=df['Geography'], columns=df['Gender'], normalize='index')
# Gender Female Male
# Geography
# France 0.426047 0.573953
# Germany 0.478408 0.521592
# Spain 0.430399 0.569601
df = df.drop(columns=['id', 'Surname'])
df = df.set_index(keys='CustomerId')
df.columns = ['Credit', 'Geography', 'Gender', 'Age', 'Tenure', 'Balance',
'NumProd', 'HasCard', 'Active', 'Salary', 'Exited']
train_num = df.select_dtypes(include=[int, float]).drop(columns='Exited')
sns.pairplot(
data=train_num, diag_kind='kde', corner=True,
diag_kws={
'color': '0.5',
'edgecolor': '0'
},
plot_kws={
'fc': '0.8',
'ec': '0',
's': 10,
'alpha': 0.2
}
)
plt.show()

df = pd.get_dummies(data=df, prefix='', prefix_sep='', dtype=int)
df = df.drop(columns='Male').rename(columns={'Female': 'Gender'})
yvar = 'Exited'
X = df.drop(columns=yvar)
y = df[yvar].copy()
from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(
X, y, test_size=0.2, random_state=0, stratify=y
)
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(random_state=0)
model.fit(X=X_train, y=y_train)
model.score(X=X_train, y=y_train)
# 0.9999166666666667
model.score(X=X_valid, y=y_valid)
# 0.9
pd.Series(
data=model.feature_importances_,
index=model.feature_names_in_
).sort_values(ascending=False)
# Age 0.302313
# NumProd 0.167192
# Salary 0.126829
# Credit 0.122589
# Balance 0.087362
# Tenure 0.068599
# Active 0.039076
# Germany 0.029903
# Gender 0.024597
# HasCard 0.014111
# France 0.010037
# Spain 0.007391
# dtype: float64
y_pred = model.predict(X=X_valid)
hds.stat.clfmetrics(y_true=y_valid, y_pred=y_pred)

hds.plot.roc_curve(y_true=y_valid, y_prob=y_prob)
hds.plot.pr_curve(y_true=y_valid, y_prob=y_prob)


| 구분 | 방식 | 장점 | 단점 | 비고 |
|---|---|---|---|---|
| Permutation Importance | 어떤 변수를 무작위로 섞어 성능 하락 정도를 측정하여 변수 중요도를 평가 | 단순하면서도 직관적 | 상관관계가 높은 변수들의 중요도를 과소평가할 수 있음 | 전역 |
| Partial Dependence | 어떤 변수의 값을 변화시키면서 평균 예측값의 변화를 관찰 | 변수의 단조적 경향과 임계값 파악 유리 직관적 시각화 가능 | 변수 간 독립성을 가정 실재하지 않는 비현실적 조합이 포함 될 수 있음 | 전역 |
| LIME | 단일 샘플 주변에서 국소 선형 모델을 학습하여 변수 기여도 추정 | 개별 사례를 직관적으로 해석 | 무작위성 및 근방 설정에 민감 | 국소 |
| SHAP | Shapley 값을 이용해 각 변수의 기여도를 공정하게 분배 | 이론적 일관성을 보장 결정 트리 계열 모델에 효율적으로 적용 | 계산량이 많아 대규모 데이터에 부담 | 전역 + 국소 |
from sklearn.inspection import permutation_importance
scoring : 기본값은 정확도‘roc_auc’ 또는 ‘neg_log_loss’ 추천pi = permutation_importance(
estimator=model, X=X_valid, y=y_valid,
random_state=0, scoring='roc_auc'
)
pd.Series(
data=pi['importances_mean'], index=model.feature_names_in_
).sort_values(ascending=False)
# NumProd 0.148834
# Age 0.107979
# Balance 0.023962
# Active 0.019315
# Germany 0.008270
# Gender 0.006819
# Credit 0.002010
# Tenure 0.001888
# Salary 0.000879
# Spain 0.000133
# France 0.000061
# HasCard -0.000566
# dtype: float64
from sklearn.inspection import PartialDependenceDisplay
features : 시각화할 변수명 리스트 지정PartialDependenceDisplay.from_estimator(
estimator=model, X=X_valid,
features=['Age', 'NumProd'],
line_kw={'color': 'red', 'linewidth': 1.5}
)
plt.show()

kindaverage : PD 곡선(기본값)individual : ICE 곡선both : 둘다average 만 가능PartialDependenceDisplay.from_estimator(
estimator=model, X=X_valid,
features=['Age', 'NumProd'],
kind='both',
random_state=0,
pd_line_kw={'color': 'red', 'linewidth': 1.5},
ice_lines_kw={'color': 'silver', 'alpha': 0.2}
)
plt.show()

'Tenure', 'Credit’ 기준
features : 이변량의 경우 DF로 입력PartialDependenceDisplay.from_estimator(
estimator=model, X=X_valid,
features=[['Age', 'NumProd']],
random_state=0,
contour_kw={'cmap': 'Greys', 'alpha': 0.5}
)
plt.show()

'Tenure', 'Credit’ 기준
from lime.lime_tabular import LimeTabularExplainer
discretize_continuous 에 True 지정 시 연속형 변수를 사분위 구간으로 나누어 범주형처럼 취급lime_explainer = LimeTabularExplainer(
training_data=X_train.values,
feature_names=model.feature_names_in_,
mode='classification',
class_names=['유지(0)', '이탈(1'],
discretize_continuous=True,
random_state=0
)
index = np.argsort(y_prob[:,1])[-1]
lime_exp = lime_explainer.explain_instance(
data_row=X_valid.values[index],
predict_fn=model.predict_proba,
num_features=5
)
from IPython.display import HTML, display
lime_exp.save_to_file('LIME_Explanation.html')
display(HTML('LIME_Explanation.html'))
pd.Series(data=X_valid.values[index], index=model.feature_importances_)
# 0.122589 553.00
# 0.302313 60.00
# 0.068599 6.00
# 0.087362 0.00
# 0.167192 1.00
# 0.014111 1.00
# 0.039076 0.00
# 0.126829 143635.33
# 0.010037 1.00
# 0.029903 0.00
# 0.007391 0.00
# 0.024597 1.00
# dtype: float64
import shap
shap 패키지에서 여러 모델에 특화된 전용 설명기를 제공shap_explainer = shap.TreeExplainer(model=model)
check_additivity=False : 시간 절약shap_values = shap_explainer.shap_values(X=X_valid, check_additivity=False)
shap_values.shape
# (3000, 12, 2)
shap_1 = pd.DataFrame(data=shap_values[:, :, 1], columns=model.feature_names_in_)
shap_1.abs().mean().sort_values(ascending=False)
# Age 0.127524
# NumProd 0.122611
# Active 0.050641
# Gender 0.035896
# Germany 0.030100
# Balance 0.020121
# Credit 0.014884
# Salary 0.012301
# Tenure 0.008378
# France 0.007261
# Spain 0.004098
# HasCard 0.004063
# dtype: float64
shap.summary_plot(
shap_values=shap_1.values,
features=X_valid,
plot_type='bar'
)

shap.summary_plot(
shap_values=shap_1.values,
features=X_valid,
plot_type='dot'
)

shap.dependence_plot(
ind='Age',
shap_values=shap_1.values,
features=X_valid,
interaction_index=None
)

shap.dependence_plot(
ind='Age',
shap_values=shap_1.values,
features=X_valid,
interaction_index='NumProd'
)

base_value = shap_explainer.expected_value[1]
base_value
# np.float64(0.20317583333333328)
pd.Series(data=shap_1.values[index, :], index=model.feature_names_in_).sort_values(ascending=False)
# Age 0.382354
# NumProd 0.183389
# Active 0.096855
# Gender 0.057051
# Credit 0.045831
# Balance 0.036531
# Salary 0.014848
# Spain 0.003778
# HasCard 0.001516
# France -0.003179
# Tenure -0.006706
# Germany -0.015444
# dtype: float64
shap.force_plot(
base_value=base_value,
shap_values=shap_1.values[index, :],
features=X_valid.iloc[index, :],
matplotlib=True
)

일주일간 이론 위주보다 제조업 현장의 이야기를 듣다가 오랜만에 이론 위주의 수업을 들으니까 평소보다 더 피곤한 것 같다. 그래도 관심 있던 주제에 대한 내용을 배우기도 했고, 새로운 시각화도 배워서 재밌게 들었던 것 같다. 내일 SQLD 시험을 마무리한 뒤, 통계부터 시작해서 이론적인 내용들을 정리하고 싶다.