혼돈행렬(Confusion Matrix)와 정밀도, 재현률, F1점수

이 글은 한빛미디어의 핸즈온 머신러닝을 수업자료로써 파악하면서 이해한 바를 짧게 요약한 글입니다. 요즘 이 책을 통해 머신러닝을 다시 접하고 있는데, 체계적이고 좋은 내용을 제공하고 있고, 나 자신을 위한 보다 명확한 이해를 돕고자 이 글을 작성 작성합니다. 요즘 제가 블로그에 올리는 머신러닝 관련 글은 대부분 이 책의 내용에 대한 나름대로의 해석을 토대로 합니다. 보다 자세한 내용은 해당 도서를 참고하기 바랍니다.

이글은 훈련된 예측 모델을 평가하기 위한 지표인 정밀도, 재현률, F1에 대한 내용입니다. 이러한 평가 지표는 혼돈행렬이라는 데이터를 토대로 계산되는데요, 먼저 혼돈행렬을 구하기 위해 학습 데이터셋이 필요하며, 0~9까지의 숫자를 손으로 작성한 MINIST를 사용하고, 이 손글씨가 7인지에 대한 예측 모델을 예로 합니다. MNIST 데이터셋을 다운로드 받고, 레이블 데이터를 재가공합니다.

from sklearn.datasets import fetch_openml
import numpy as np

mnist = fetch_openml('mnist_784', version=1, data_home='D:/__Temp__/_')

X, y = mnist["data"], mnist["target"]
y = y.astype(np.uint8)

X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
y_train_7 = (y_train == 7)
y_test_7 = (y_test == 7)

예측 모델은 SGDClassifier를 사용합니다.

from sklearn.linear_model import SGDClassifier

model = SGDClassifier(random_state=3224)

혼돈 행렬을 얻기 위해 다음 코드를 실행합니다.

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix

y_train_pred = cross_val_predict(model, X_train, y_train_7, cv=3)
cf = confusion_matrix(y_train_7, y_train_pred)
print(cf)
[[53223   512]
 [  726  5539]]

cross_val_predict 함수는 아직 전혀 학습이 되지 않은 모델을 지정된 교차검증 수만큼 학습시킨 뒤 예측값을 반환합니다. 이렇게 얻은 예측값과 실제 값을 비교해서 얻은 혼돈행렬의 결과에 대한 상세한 이미지는 아래와 같습니다.

위의 그림에서 표에 담긴 4개의 값은 발생횟수입니다. TN과 TP의 값은 옳바르게 예측한 횟수이고 FN과 FP는 잘못 예측한 횟수입니다. 즉, FN과 FP가 0일때 모델은 완벽하다는 의미입니다.

이제 위의 혼돈행렬에서 정밀도(Precision)와 재현률(Recall), F1점수에 대한 수식은 다음과 같습니다.

    $$Precision=\frac{TP}{TP+FP}, Recall=\frac{TP}{TP+FN},F1=2\times\frac{Precision \times Recall}{Precision+Recall}$$

정밀도와 재현률이 서로 상반관계에 있습니다. 즉, 정밀도가 높으면 재현률이 떨어지며 재현률이 높아지면 정밀도가 떨어지는 경향이 있습니다. F1은 이런 상반관계에 있는 정밀도와 재현률을 묶어 평가하고자 하는 지표입니다.

비록 정밀도와 재현률, F1점수는 매우 단순해 계산하기 쉬우나 다음의 코드를 통해서도 쉽게 얻을 수 있습니다.

from sklearn.metrics import precision_score, recall_score, f1_score
p = precision_score(y_train_7, y_train_pred)
print(p)
r = recall_score(y_train_7, y_train_pred)
print(r)
f1 = f1_score(y_train_7, y_train_pred)
print(f1)
0.9153858866303091
0.8841181165203511
0.8994803507632347

OneHot 인코딩(Encoding) 및 스케일링(Scaling)

학습 데이터의 특성들은 수치값 뿐만 아니라 ‘크다’, ‘중간’, ‘작다’ 또는 ‘여자’, ‘남자’와 같은 범주값도 존재합니다. 먼저 범주형 값을 처리하기 위해서는 이 범주형 값을 수치값으로 변환해야 합니다. 만약 범주형 값이 ‘A등급’, ‘B등급’, ‘C등급’처럼 그 의미에 순위적 연속성이 존재한다면 그냥 3, 2, 1과 같이 수치값으로 등급을 매칭하면 됩니다. 하지만 ‘여자’, ‘남자’처럼 순위도 연속성도 없다면 반드시 다른 의미로의 수치값으로 변환해야 하는데, 그 변환은 OnHot 인코딩이라고 합니다. 결론을 미리 말하면 ‘여자’라면 벡터 (1,0)으로, 남자라면 (0,1)으로 변경해야 합니다.

샘플 데이터를 통해 이 OneHot 인코딩을 하는 방법에 대해 언급하겠습니다. 샘플 데이터는 아래의 글에서 소개한 데이터를 사용합니다.

분석가 관점에서 데이터를 개략적으로 살펴보기

먼저 샘플 데이터를 불러옵니다.

import pandas as pd

raw_data = pd.read_csv('./datasets/datasets_1495_2672_abalone.data.csv', 
        names=['sex', 'tall', 'radius', 'height', 'weg1', 'weg2', 'weg3', 'weg4', 'ring_cnt'])
    #names=['성별', '키', '지름', '높이', '전체무게', '몸통무게', '내장무게', '껍질무게', '껍질의고리수']

이 중 sex 컬럼은 성별인데, 이 컬럼의 값을 아래의 코드를 통해 출력해 봅니다.

print(raw_data["sex"][:10])
0    M
1    M
2    F
3    M
4    I
5    I
6    F
7    F
8    M
9    F

범주형 값이라는 것을 알수있는데, M은 숫컷, F는 암컷, I는 유충입니다. 이 sex 컬럼에 대해 OneHot 인코딩 처리를 위해 먼저 문자형을 숫자형으로 변환해주는 OrdinalEncoder 클래스를 통해 처리합니다.

raw_data_labels = raw_data["ring_cnt"].copy()
raw_data = raw_data.drop("ring_cnt", axis=1)

raw_data_cat = raw_data[["sex"]]

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = OrdinalEncoder()
raw_data_encoded = ordinal_encoder.fit_transform(raw_data_cat)

print(raw_data_encoded[:10])
print(ordinal_encoder.categories_)
[[2.]
 [2.]
 [0.]
 [2.]
 [1.]
 [1.]
 [0.]
 [0.]
 [2.]
 [0.]]
[array(['F', 'I', 'M'], dtype=object)]

OrdinalEncoder는 범주형 데이터를 희소행렬(Sparse Matrix)로 그 결과를 반환합니다. 다시 이 희소행렬을 OneHot 인코딩을 시키기 위해 아래의 코드를 수행합니다.

from sklearn.preprocessing import OneHotEncoder
onehot_encoder = OneHotEncoder()
raw_data_cat_onehot = onehot_encoder.fit_transform(raw_data_cat)
print(raw_data_cat_onehot.toarray()[:10])
print(onehot_encoder.categories_)
[[0. 0. 1.]
 [0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]
 [0. 1. 0.]
 [0. 1. 0.]
 [1. 0. 0.]
 [1. 0. 0.]
 [0. 0. 1.]
 [1. 0. 0.]]
[array(['F', 'I', 'M'], dtype=object)]

이제 범주형 컬럼인 sex 대신 OneHot 인코딩된 값을 데이터에 추가하도록 합니다.

raw_data = raw_data.drop("sex", axis=1)
raw_data = np.c_[raw_data_cat_onehot.toarray(), raw_data]
print(raw_data[:10])
[[0.0 0.0 1.0 0.455 0.365  0.095 0.514  0.2245 0.100999 0.15  4]
 [0.0 0.0 1.0 0.35  0.265  0.09  0.2255 0.0995 0.0485   0.07  2]
 [1.0 0.0 0.0 0.53  0.42   0.135 0.677  0.2565 0.1415   0.21  4]
 [0.0 0.0 1.0 0.44  0.365  0.125 0.516  0.2155 0.114    0.155 4]
 [0.0 1.0 0.0 0.33  0.255  0.08  0.205  0.0895 0.0395   0.055 2]
 [0.0 1.0 0.0 0.425 0.3    0.095 0.3515 0.141  0.0775   0.12  3]
 [1.0 0.0 0.0 0.53  0.415  0.15  0.7775 0.237  0.1415   0.33  4]
 [1.0 0.0 0.0 0.545 0.425  0.125 0.768  0.294  0.1495   0.26  4]
 [0.0 0.0 1.0 0.475 0.37   0.125 0.5095 0.2165 0.1125   0.165 4]
 [1.0 0.0 0.0 0.55  0.44   0.15  0.8945 0.3145 0.151    0.32  4]]

범주형 타입인 sex가 제거되고 이 sex에 대한 추가적인 3개의 컬럼이 추가되었습니다. 바로 이 3개의 컬럼이 OneHot 인코딩된 값입니다.

이제 수치형 데이터에 대한 스케일링입니다. 여기서 스케일링이란 서로 다른 특성들을 일정한 값의 범위로 맞춰주는 것입니다. 흔히 사용하는 방식은 Min-Max 스케일링과 표준화(Standardization)이 있습니다. 먼저 Min-Max 스케일링은 특징의 최소값과 최대값을 먼저 계산하고 이 값을 이용하여 전체 특징값들을 0~1 사이의 값으로 변경시킵니다. 표준화는 먼저 평균과 표준편차를 구하고 전체 데이터 각각에 대해 평균을 뺀 후 표준편차로 나눠 분산이 1이 되도록 데이터를 조정합니다. 각각 sklearn에서 제공하는 MinMaxScaler와 StandardScaler 클래스를 통해 수행이 가능합니다. 아래의 코드는 Min-Max 스케일링을 수행하는 코드입니다.

from sklearn.preprocessing import MinMaxScaler

minmax_scaler = MinMaxScaler()
raw_data = minmax_scaler.fit_transform(raw_data)

print(raw_data[:10])
[[0. 0.  1.   0.51351351 0.5210084  0.0840708   0.18133522 0.15030262 0.1323239  0.14798206 0.75 ]
 [0. 0.  1.   0.37162162 0.35294118 0.07964602  0.07915707 0.06624075 0.06319947 0.06826109 0.25 ]
 [1. 0.  0.   0.61486486 0.61344538 0.11946903  0.23906499 0.17182246 0.18564845 0.2077728  0.75 ]
 [0. 0.  1.   0.49324324 0.5210084  0.11061947  0.18204356 0.14425017 0.14944042 0.15296462 0.75 ]
 [0. 1.  0.   0.34459459 0.33613445 0.07079646  0.07189658 0.0595158  0.05134957 0.0533134  0.25 ]
 [0. 1.  0.   0.47297297 0.41176471 0.0840708   0.12378254 0.09414929 0.10138249 0.1180867  0.5  ]
 [1. 0.  0.   0.61486486 0.60504202 0.13274336  0.27465911 0.15870881 0.18564845 0.32735426 0.75 ]
 [1. 0.  0.   0.63513514 0.62184874 0.11061947  0.27129449 0.19704102 0.1961817  0.25759841 0.75 ]
 [0. 0.  1.   0.54054054 0.52941176 0.11061947  0.17974146 0.14492266 0.14746544 0.16292975 0.75 ]
 [1. 0.  0.   0.64189189 0.64705882 0.13274336  0.31609704 0.21082717 0.19815668 0.31738914 0.75 ]]

결과를 보면 전체 데이터가 모두 0~1 사이의 값으로 변환된 것을 알 수 있습니다.