Py) ML - 모델 평가(Permutation Importance)

Py) ML - 모델 평가(Permutation Importance)

지도학습 모델의 독립변수 중요도를 확인하기 위해 사용하는 Permutation Importance에 대해 알아본다.


예제 파일: car_bmw3.csv

개요

지도학습 모델을 평가할 때 종속변수를 기반으로 해당 모델의 전반적인 성능을 비교하는 것이 일반적이다. 그리고 더 나아가서 입력된 독립변수의 중요도가 어떠한지 확인하는 작업을 하기도 한다.
모델평가(분류모델) 게시물 확인하기
모델평가(회귀모델) 게시물 확인하기

모델을 만든다고 해서 최종적으로 모든 독립변수를 사용하는 것은 아니다. 어떤 독립변수는 해석하기가 난해할 수 있고, 어떤 독립변수는 (가공 또는 확보)비용이 많이 들 수 있고, 어떤 독립변수는 영향력이나 중요도가 상대적으로 낮아 굳이 필요가 없을 수도 있기에 여러 판단기준에 따라 불필요 하다고 판단되는 독립변수를 모델에서 제외하기도 한다.

이는 모델의 경량화나 정비를 위해 실시하는 특성 공학(Feature Engineering)과 매우 관련이 깊으며 보다 나은 모델을 확보하기 위해 독립변수의 중요도를 아는 것은 중요하다. 그 중요도를 파악하기 위한 방법 중 지도학습 모델에 공통적으로 사용할 수 있는 방법은 대표적으로 두 가지가 있으며 이는 다음과 같다.

  1. Permutation Importance
  2. Drop Column Importance

이 게시글에서는 Permutation Importance에 대해 다룬다. 그럼 이제 생소할 수 있는 영단어 “Permutation”을 짚어보자. 사전을 검색하면 다음과 같이 “Permutation”에는 “치환”이라는 뜻이 있다.

Permutation: 순열, 치환

그래서 굳이 한글로 번역을 하자면 “치환 중요도” 라고 할 수 있겠다. (하지만 아직 생소하고 어색하다.) 아무튼 그러면 어떤 것을 치환하는 것일까?

바로 평가 데이터세트(test dataset)의 특정 변수를 치환하는 것이다.

다음의 예시를 통해 치환 전과 후의 데이터로 이해를 해보자.
변수 치환 예시

상기 치환 예시에서는 “경력”변수의 값을 섞어보았다. 첫 번째 사람의 경우 나이 52세, 경력 24년, 실적 45개였지만 경력에 다른 값이 들어와서 나이 52세, 경력 1년, 실적 45개 로 되었다. 즉, 경력 1년인데 실적이 너무 많고, 나이가 52세인데 경력이 1년 밖에 되지 않는 실제 데이터라고 보기 어려운 데이터가 만들어지는 꼴이 되는 것이다. 이렇게 특정 변수의 값을 뒤섞어버리면 해당 변수와 다른 변수와의 관계가 끊어진다고 보고 이 데이터를 기반으로 평가지표를 산출하면 대체적으로 섞기 전과 대비해서 그 값이 안좋아진다.

장점

  1. 모델에 종속되지 않음
  2. 연산비용이 적게 듦

모델에 종속되지 않는 것은 모델의 독립변수별 중요도 평가 방법을 지도학습의 어떠한 모델에도 사용할 수 있다는 것이다. 예를 들어 랜덤포레스트(random forest)의 경우 MDI(Mean Decrease in Impurity)가 있긴 하지만 이는 랜덤포레스트(또는 트리모델)한정으로 사용하는 기법이다. 그래서 이런 자체적인 독립변수 중요도 평가 방법이 존재하지 않는 모델의 경우 Permutation Importance를 사용하는 것이 좋은 선택이 될 수 있다.

Permutation Importance의 큰 장점은 모델의 학습을 단 한번만 해도 된다는 것이다. 예를 들어 Drop Column Importance의 경우 독립변수 개수 + 1번의 학습이 필요하다. 반면 평가 데이터세트(test dataset)만 사용해서 계산하는 Permutation Importance의 경우 그 이점이 학습 비용이 큰 모델의 경우 극대화 된다고 할 수 있겠다.

단점

Permutation Importance에는 임의확률과정(random stochastic process)이 있기 때문에 그에 수반하는 불확실성이 있기 마련이다. 그 대표적인 특징 중 하나가 특정 변수를 완전히 제거하고 학습하는 것이 아니라 단순히 섞는 것이기 때문에 재현을 위한 seed 번호를 고정하지 않으면 중요도를 계산할 때 마다 다른 값이 나오게 되는 것이다. 그래서 한 번의 측정으로 단정짓기 어렵기 때문에 중요도 계산을 여러번 해서 그 평균을 보기도 한다.

이 즈음 되면 Permutation Importance의 장점인 낮은 연산비용이 과연 장점이 맞는지 의심스러울 수 있다. 학습비용이 매우 큰 경우는 Drop Columns Importance에 비해 연산비용에서 이점이 있겠으나 그렇지 않은 경우는 특별하게 장점이라고 하기 어렵다. 그래도 실무에서 일회성으로 하는 내부작업의 경우 한 번 연산에 3~5분 내로 끝나는 연산은 비용 어쩌고를 크게 신경쓰지 않는다.

그리고 평가 데이터세트의 특정 변수를 섞어서 그 관계를 끊어버린다는 개념이 Drop Columns Importance에서 특정 변수를 그냥 제거해버리는 것 대비 생소하면서 어렵게 느껴질 수 있다는 단점이 있다.

실습

실습의 코드는 수정중이니 참고만 하도록 하자.

직접 구현하기

직접 모델을 생성하고 구현해보자.

모델 학습 및 준비

먼저 데이터와 모델을 준비하자. 데이터 “car_bmw3.csv”는 본 게시물 상단 링크를 통해 다운로드 하도록 하자.

1
2
3
4
5
6
7
8
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.inspection import permutation_importance

df = pd.read_csv("car_bmw3.csv")
df.head(2)
year price transmission mileage fuelType tax mpg engineSize
0 2017 16500 Manual 16570 Diesel 125 58.9 2.0
1 2017 14250 Automatic 55594 Other 135 148.7 2.0

명목형 변수를 가변수(dummy variable)로 바꾸고 종속변수로 사용할 “price” 변수를 첫 번째로 옮겨보자.

1
2
3
4
5
6
df_dum = pd.get_dummies(df, columns = ["transmission", "fuelType"], dtype = "int")
df_dum = df_dum.set_index("price").reset_index()
print(df_dum.shape)
## (2443, 13)

df_dum.iloc[:3, :8]
price year mileage tax mpg engineSize transmission_Automatic transmission_Manual
0 16500 2017 16570 125 58.9 2.0 0 1
1 14250 2017 55594 135 148.7 2.0 1 0
2 16000 2017 45456 30 64.2 2.0 1 0

데이터세트를 분리하고 모델을 생성하고 RMSE를 계산한 결과 RMSE가 3577.4인 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
df_train, df_test = train_test_split(df_dum, train_size = 0.7, random_state = 123)
len(df_train), len(df_test)
## (1710, 733)

model_lr = LinearRegression()
model_lr.fit(X = df_train.drop(columns = "price"),
y = df_train["price"])
pred = model_lr.predict(df_test.drop(columns = "price"))
rmse_full = mean_squared_error(y_true = df_test["price"], y_pred = pred) ** 0.5
round(rmse_full, 1)
## 3577.4

1회성 평가

모델과 데이터가 준비되었으니 이제 각 독립변수의 중요도를 계산해보자.

첫 번째 종속변수를 제외하고 독립변수를 하나씩 차례대로 섞고 해당 데이터를 기반으로 RMSE를 계산한 결과를 “ls_rmse” 객체에 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
df_test_i = df_test.copy()

ls_rmse = []
for n_col in range(1, df_test_i.shape[1]):
df_test_i.iloc[:, n_col] = df_test_i.iloc[:, n_col].sample(n = df_test_i.shape[0],
random_state = 123)
arr_pred = model_lr.predict(df_test_i.drop(columns = "price"))
val_rmse = mean_squared_error(y_true = df_test_i["price"], y_pred = arr_pred) ** 0.5
ls_rmse = ls_rmse + [val_rmse]

ls_rmse
## [7441.402641963798,
## 10252.543020025616,
## 10292.282401647197,
## 12373.568978754274,
## 12495.832381037773,
## 12441.122697776089,
## 12622.148782736462,
## 12775.591817210334,
## 13806.16376614915,
## 13644.57162716121,
## 13518.51400006522,
## 11896.944517175827]

이제 그 결과를 “df_pref” 객체에 정리해본다. 좀 더 보기편하게 하기 위해 데이터프레임 객체에 스타일링을 적용하였다.
※ Pandas Styling 시리즈 게시물 → 클릭
※ 경우에 따라 jinja2 패키지 설치 또는 업그레이드가 필요할 수 있다.
※ “!pip install jinja2 –upgrade”

1
2
3
4
5
df_perf = pd.DataFrame(dict(col_name = df_test_i.columns[1:],
rmse = ls_rmse))
df_perf["diff"] = rmse_full - df_perf["rmse"]
df_perf["diff_nor"] = (df_perf["diff"] - df_perf["diff"].min()) / (df_perf["diff"].max() - df_perf["diff"].min())
df_perf.style.bar(color = "#FFA07A", subset = "diff_nor", align = 0)
  col_name rmse diff diff_nor
0 year 7441.402642 -3864.029732 1.000000
1 mileage 10252.543020 -6675.170110 0.558327
2 tax 10292.282402 -6714.909492 0.552084
3 mpg 12373.568979 -8796.196069 0.225082
4 engineSize 12495.832381 -8918.459471 0.205873
5 transmission_Automatic 12441.122698 -8863.749788 0.214469
6 transmission_Manual 12622.148783 -9044.775873 0.186027
7 transmission_Semi-Auto 12775.591817 -9198.218908 0.161918
8 fuelType_Diesel 13806.163766 -10228.790857 0.000000
9 fuelType_Hybrid 13644.571627 -10067.198718 0.025389
10 fuelType_Other 13518.514000 -9941.141091 0.045194
11 fuelType_Petrol 11896.944517 -8319.571608 0.299967

RMSE 변화분을 기준으로 정리한 것을 “diff” 변수에 기록하였고 해당 변수를 정규화 한 것이 “diff_nor”이다.

반복 평가 및 취합

반복문을 통해 결괏값을 여러번 생산하고 이를 취합하는 코드를 작성해보자.

각 변수당 100번 반복하고 평균을 산출했다. 그리고 재현을 위해 “random_state”에도 똑같은 값이 할당될 수 있도록 객체를 입력받도록 코드를 수정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
df_test_i = df_test.copy()

ls_rmse = []
for n_col in range(1, df_test_i.shape[1]):

ls_rmse_sub = 0
for n_iter in range(100):
df_test_i.iloc[:, n_col] = df_test_i.iloc[:, n_col].sample(n = df_test_i.shape[0],
random_state = n_iter)
arr_pred = model_lr.predict(df_test_i.drop(columns = "price"))
val_rmse_sub = mean_squared_error(y_true = df_test_i["price"],
y_pred = arr_pred) ** 0.5
ls_rmse_sub = ls_rmse_sub + val_rmse_sub

ls_rmse = ls_rmse + [ls_rmse_sub / 100]

df_perf2 = pd.DataFrame(dict(col_name = df_test_i.columns[1:],
rmse = ls_rmse))
df_perf2["diff"] = rmse_full - df_perf2["rmse"]
df_perf2["diff_nor"] = (df_perf2["diff"] - df_perf2["diff"].min()) / (df_perf2["diff"].max() - df_perf2["diff"].min())
df_perf2.style.bar(color = "#FFA07A", subset = "diff_nor", align = 0)
  col_name rmse diff diff_nor
0 year 7351.545386 -3774.172477 1.000000
1 mileage 9253.400649 -5676.027740 0.690200
2 tax 10169.236944 -6591.864034 0.541017
3 mpg 11858.459581 -8281.086671 0.265853
4 engineSize 12202.791854 -8625.418944 0.209764
5 transmission_Automatic 12196.684713 -8619.311803 0.210759
6 transmission_Manual 12258.714096 -8681.341187 0.200655
7 transmission_Semi-Auto 12410.675151 -8833.302241 0.175901
8 fuelType_Diesel 13490.530160 -9913.157250 0.000000
9 fuelType_Hybrid 13374.008981 -9796.636072 0.018981
10 fuelType_Other 13216.422588 -9639.049679 0.044650
11 fuelType_Petrol 12796.844785 -9219.471875 0.112997

sklearn 활용

sklearn 라이브러리의 permutation_importance() 함수를 사용하여 Permutation Importance를 계산할 수 있다.
※ 공식문서 페이지 → Click

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# https://scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter
from sklearn.metrics import make_scorer
RMSE_scorer = make_scorer(mean_squared_error, squared = False,
greater_is_better = False)

dic_result = permutation_importance(model_lr,
X = df_test.drop(columns = "price"),
y = df_test["price"],
scoring = RMSE_scorer,
n_repeats = 100,
random_state = 123)

df_result = pd.DataFrame(dict(col_name = df_test.columns[1:],
imp = dic_result["importances_mean"]))
df_result["diff_nor"] = (df_result["imp"] - df_result["imp"].min()) / (df_result["imp"].max() - df_result["imp"].min())
df_result.style.bar(color = "#FFA07A", subset = "diff_nor", align = 0)
  col_name imp diff_nor
0 year 3793.396413 1.000000
1 mileage 1519.115507 0.399973
2 tax 7.648989 0.001200
3 mpg 2861.775300 0.754209
4 engineSize 217.922042 0.056677
5 transmission_Automatic 3.100185 0.000000
6 transmission_Manual 52.262604 0.012971
7 transmission_Semi-Auto 31.196901 0.007413
8 fuelType_Diesel 2185.217117 0.575711
9 fuelType_Hybrid 390.595930 0.102234
10 fuelType_Other 160.398965 0.041500
11 fuelType_Petrol 3208.713414 0.845742
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×