Data Science/Machine Learning

Evaluation1 - 분류 모델 성능 지표 (Accuracy, Confusion Matrix, Precision, Recall, F1 score, ROC AUC )

HJChung 2020. 9. 28. 16:27

권철민 강사님의 '파이썬 머신러닝 완벽 가이드'을 학습하고 정리한 것입니다. 배우는 중이라 잘못된  내용이 있을 수 있으며 계속해서 보완해 나갈 것입니다. :)) 

 

머신러닝의 프로세스는

- 데이터 가공/ 변환

- 모델 학습/예측

- 평가 

로 이루어진다. 

 

이 중 <평가>에 대해 학습 한 것을 정리해보고자 한다. 

 

머신러닝 모델의 성능을 평가하는 지표인 성능 평가 지표는 모델이 분류모델이냐/ 회귀 모델이냐에 따라 달라진다. 

 

- 분류의 성능 평가 지표: 정확도(Accuracy), 오차 행렬(Confusion Matrix), 정밀도(Precision), 재현율(Recall), F1 score, ROC AUC 

- 회귀의 성능 평가 지표: 대부분 실제 값과 예측값의 오차 평균값에 기반한 평가

 

먼저 분류의 성능 평가 지표에 대해 정리해보자 

1. 분류의 성능 평가 지표

1) 정확도(Accuracy)

정확도 = (예측 결과가 동일한 데이터 건수) / (전체 예측 데이터 건수)

sklearn.metrics.accuracy_score(y_true, y_pred, *, normalize=True, sample_weight=None)

정확도는 실제 데이터에 예측 데이터가 얼마나 같은지를 판단하는 지표이다. 

정확도라는 것이 모델의 예측 성능을 나타내는 매우 직관적인 평가 지표이긴 하지만 이진분류 인 경우 정확도로만 평가하기엔 무리가 있다. 왜냐하면 데이터의 구성에 따라 모델의 성능이 왜곡되게 평가될 수도 있기 때문이다. 

특히 데이터 분균형이 심한 경우 이 지표는 더욱더 믿을만한게 못된다. 

(예를 들어 영상 데이터에서 covid 와 none을 classification 하는 문제가 있을 때, covid가 전체 데이터의 10%, none이 전체 데이터의 90%라면 모두 none으로 판단하는 모델이라도 정확도는 90%이 나오게 된다. )

 

그래서 정확도만 사용하기보다 여러 분류 성능 지표와 함께 사용하는게 좋다. 

 

2) 오차 행렬(Confusion Matrix)

오차 행렬은 학습된 분류 모델이 예측을 수행하면서 얼마나 헷갈리고 있는지도 함께 보여주는 지표이다. 

sklearn.metrics.confusion_matrix(y_true, y_pred, *, labels=None, sample_weight=None, normalize=None)

※ 일반적으로 불균형하면서 이진 분류를 해야하는 데이터는 중점적으로 찾아야하는 매우 적은 결과 값을 가지는 데이터를 Positive로, 그렇지 않는 데이터를 Negative로 설정한다. 

 

 행렬의 각 칸에 적힌 TP, FP, FN, TN 중 뒤 글자인 P, N은 '예측을 이렇게 했다'라는 의미이다. 

그리고 앞 글자인 T, F는  '그 예측 결과가 실제 값과 일치하거나(T), 다름(F)을 의미한다.

- TN: 예측 값을 Negatice 값 0으로 예측, 실제 값은 Negative값인 0

- FP: 예측 값을 Positive 값 1로 예측, 실제 값은 Negative값인 0

- FN: 예측 값을 Negative 값 0으로 예측, 실제 값은 Positive 값인 1

- TP: 예측 값을 Positive 값 1로 예측, 실제 값은 Positive값인 1

그래서 예측 결과와 실제 값이 동일한 것은 TN, TP이 되겠다. 

오차 행렬로 해당 모델이 어떻게 판단하는지를 알 수 있었고, 정확도가 왜 높게 나왔는지, 그 정확도가 타당한지 등을 알아볼 수 있는 방법인 듯 하다. 그러나 모델 성능 지표로 사용하기에 한계가 있는 것은 여전하다. 

그래서 불균형한 데이터 세트에서는 '정밀도'와 '재현율' 성능 평가 지표가 더 선호된다. 

 

3) 정밀도(Precision)와 재현율(Recall; Sensitivity; TPR)

 

정밀도 = TP / (TP + FP)

sklearn.metrics.precision_score(y_true, y_pred, *, labels=None, pos_label=1, average='binary', sample_weight=None, zero_division='warn')

정밀도는 예측 값을 Positive로 한 것 중에 예측 값과 실제 값이 positive로 일치한 데이터의 비율을 의미한다. 

※ 정밀도가 더 중요하게 사용되는 경우

실제 Negative 데이터를 Positive로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우

ex) 스팸메일 여부 판단 - 실제 업무메일(Positive)인데 스팸(Negative)로 판단하면 아에 받지를 못하고 업무상 큰일.

 

재현율 = TP / (FN + TP)

sklearn.metrics.recall_score(y_true, y_pred, *, labels=None, pos_label=1, average='binary', sample_weight=None, zero_division='warn')

재현율은 실제 값이 Positive인 것 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율을 의미한다. 

※ 재현율이 더 중요하게 사용되는 경우

실제 Positive 데이터를 Negative로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우

ex) 암 판단 모델 - 실제 암 환자(Positive)인데 정상(Negative)로 판단하면 생명을 앗아갈 정도로 큰일. 

 

정밀도를 높이려면? TP도 높여야하지만, 분모도 작아져야 한다. 그러니까 FP를 낮추는 데 중점을 두게 된다.

재현율을 높이려면? TP도 높여야하지만, 분모도 작아져야 한다. 그러니까 FN를 낮추는 데 중점을 두게 된다. 

그래서 정밀도와 재현율은 서로 보완적인 분류 성능 지표이며 가장 좋은 모델은 재현율과 정밀도의 수치가 둘다  높은 것이다.

 

또한 업무 특성에 따라 더 강조되어야 할 성능 지표는 분류의 결정 임계값(Threshold)를 조정해서 해당 수치를 높일 수 있다. 

그런데 정밀도와 재현율은 독립적이지 않기 때문에 어느 한 쪽을 강제로 높이면 다른 하나의 수치는 떨어질 수도 있다. (Trade off)

 

※ skelarn의 precision_recall_curve() 함수를 통해 결정 임계값에 따라 정밀도, 재현율의 변화값이 어떻게 변하는지를 보면서 진행할 수 있다. 

sklearn.metrics.precision_recall_curve(y_true, probas_pred, *, pos_label=None, sample_weight=None)

사용 예시)

scikit-learn.org/stable/auto_examples/model_selection/plot_precision_recall.html#sphx-glr-auto-examples-model-selection-plot-precision-recall-py

 

Precision-Recall — scikit-learn 0.23.2 documentation

Note Click here to download the full example code or to run this example in your browser via Binder Precision-Recall Example of Precision-Recall metric to evaluate classifier output quality. Precision-Recall is a useful measure of success of prediction whe

scikit-learn.org

이렇게 임계값을 변형하여 정밀도와 재현율을 높일 수는 있지만 수치만 높이는데 집중하는 것은 바람직 하지 않다. 얼마든지 극단적으로 만들 수 있기 때문이다. 

ex) 암 예측 모델에서 재현율을 높이고자 삑하면 양성으로 판단하게 만들 경우 환자의 신뢰를 얻기는 힘들 것이다. 

 

그래서 정밀도와 재현율의 수치가 적절하게 조합된 종합 성능 평가 지표가 필요하다. 

 

4)  F1 score

정밀도와 재현율을 결합한 지표로, 정밀도와 재현율이 어느 한 쪽으로 치우치지 않을 때 상대적으로 높은 값을 가진다. 

sklearn.metrics.f1_score(y_true, y_pred, *, labels=None, pos_label=1, average='binary', sample_weight=None, zero_division='warn')

출처: https://www.google.com/url?sa=i&url=https%3A%2F%2Fpmirla.github.io%2F2018%2F11%2F15%2Fprecison_recall.html&psig=AOvVaw1zkkjhAXEs57lE-JeqCrZA&ust=1602560747764000&source=images&cd=vfe&ved=0CAIQjRxqFwoTCIDfu5WSruwCFQAAAAAdAAAAABAt

 

5) ROC 곡선과 AUC

ROC 곡선과 이에 기반한 AUC 는 의학 분야나 이진 분류 모델의 성능 평가 지표로 중요하게 사용된다. 

ROC 곡선은 FPR의 변화에 따른 TRP의 변화를 나타내는 곡선이다. 

※ FPR (False Positive Rate): FPR = FP / (FP+TN) 으로, 실제 Negative 를 잘못 예측한 비율을 나타낸다. 

※ TPR (True Positive Rate): TPR = TP / (FN+TP)으로, 재현율과 같이 실제 Positive인 것 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율을 의미한다. 

ROC 곡선은 FPR과 TPR의 변화 값을 보는데 이용하며, 분류 모델의 성능 지표로 사용되는 것은 AUC값이다. 

AUC(Area Under Curve)는 ROC 곡선 밑의 면적으로 구한 것으로서 일반적으로 1에 가까울 수록 좋은 것이고, 이를 높이려면 FPR이 작은 상태에서 큰 TRP을 얻어야 한다. 

 

ROC 곡선 데이터 

sklearn.metrics.roc_curve(y_true, y_score, *, pos_label=None, sample_weight=None, drop_intermediate=True)[source]

AUC 값 

sklearn.metrics.roc_auc_score(y_true, y_score, *, average='macro', sample_weight=None, max_fpr=None, multi_class='raise', labels=None)

출처: 파이썬 머신러닝 완벽가이드

2. 분류의 성능 평가 지표 예시 코드 

 

 

 

 

Accuracy/ Confusion matrix/ Precision/ Recall Trade-off

In [1]:
import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score, recall_score
In [2]:
# 평가 성능 지표를 위한 function

def get_classifier_eval(y_test, y_pred):
    accuracy = accuracy_score(y_test, y_pred)
    confusion = confusion_matrix(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    print('오차 행렬')
    print(confusion)
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}'.format(accuracy , precision ,recall))
In [3]:
# Data Preprocessing 

# Null 처리 함수
def fillna(df):
    df['Age'].fillna(df['Age'].mean(),inplace=True)
    df['Cabin'].fillna('N',inplace=True)
    df['Embarked'].fillna('N',inplace=True)
    df['Fare'].fillna(0,inplace=True)
    return df

# 머신러닝 알고리즘에 불필요한 속성 제거
def drop_features(df):
    df.drop(['PassengerId','Name','Ticket'],axis=1,inplace=True)
    return df

# 레이블 인코딩 수행. 
def format_features(df):
    df['Cabin'] = df['Cabin'].str[:1]
    features = ['Cabin','Sex','Embarked']
    for feature in features:
        le = LabelEncoder()
        le = le.fit(df[feature])
        df[feature] = le.transform(df[feature])
    return df

# 앞에서 설정한 Data Preprocessing 함수 호출
def transform_features(df):
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df
In [4]:
titanic_df = pd.read_csv('../1장/titanic/train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df= titanic_df.drop('Survived', axis=1)
X_titanic_df = transform_features(X_titanic_df)
In [5]:
X_train, X_test, y_train, y_test = train_test_split(X_titanic_df, y_titanic_df, \
                                                    test_size=0.20, random_state=11)

lr_clf = LogisticRegression()

lr_clf.fit(X_train , y_train)
pred = lr_clf.predict(X_test)
get_classifier_eval(y_test , pred)
 
오차 행렬
[[104  14]
 [ 13  48]]
정확도: 0.8492, 정밀도: 0.7742, 재현율: 0.7869
 
/Users/jeonghyeonjeong/opt/anaconda3/envs/pythonML/lib/python3.8/site-packages/sklearn/linear_model/_logistic.py:762: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
 

Precision/Recall Trade-off

predict_proba( ) 메소드:

이진 분류일 때 0일 때의 확률과 1일 때의 확률을 반환.

In [6]:
pred_proba = lr_clf.predict_proba(X_test)
pred  = lr_clf.predict(X_test)
print('pred_proba()결과 Shape  {0}'.format(pred_proba.shape))
print('pred_proba array에서 앞 3개만 샘플로 추출 \n [0이 될 확률, 1이 될 확률] \n :', pred_proba[:3])

# 예측 확률 array 와 예측 결과값 array 를 concatenate 하여 예측 확률과 결과값을 한눈에 확인
pred_proba_result = np.concatenate([pred_proba , pred.reshape(-1,1)],axis=1)
print('두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 \n [0이 될 확률, 1이 될 확률, class 결정값] \n :',pred_proba_result[:3])
 
pred_proba()결과 Shape  (179, 2)
pred_proba array에서 앞 3개만 샘플로 추출 
 [0이 될 확률, 1이 될 확률] 
 : [[0.46184106 0.53815894]
 [0.87866995 0.12133005]
 [0.8771695  0.1228305 ]]
두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 
 [0이 될 확률, 1이 될 확률, class 결정값] 
 : [[0.46184106 0.53815894 1.        ]
 [0.87866995 0.12133005 0.        ]
 [0.8771695  0.1228305  0.        ]]
In [7]:
#Binarizer을 이용해서 threshold에 따라 최종 예측값을 다르게 구할 수 있다. 
# 테스트를 수행할 모든 임곗값을 리스트 객체로 저장. 

from sklearn.preprocessing import Binarizer

thresholds = [0.4, 0.45, 0.50, 0.55, 0.60]

def get_eval_by_threshold(y_test , pred_proba_c1, thresholds):
    # thresholds list객체내의 값을 차례로 iteration하면서 Evaluation 수행.
    for custom_threshold in thresholds:
        binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_c1) 
        custom_predict = binarizer.transform(pred_proba_c1)
        print('임곗값:',custom_threshold)
        get_classifier_eval(y_test , custom_predict)

get_eval_by_threshold(y_test ,pred_proba[:,1].reshape(-1,1), thresholds )
 
임곗값: 0.4
오차 행렬
[[98 20]
 [10 51]]
정확도: 0.8324, 정밀도: 0.7183, 재현율: 0.8361
임곗값: 0.45
오차 행렬
[[103  15]
 [ 12  49]]
정확도: 0.8492, 정밀도: 0.7656, 재현율: 0.8033
임곗값: 0.5
오차 행렬
[[104  14]
 [ 13  48]]
정확도: 0.8492, 정밀도: 0.7742, 재현율: 0.7869
임곗값: 0.55
오차 행렬
[[109   9]
 [ 15  46]]
정확도: 0.8659, 정밀도: 0.8364, 재현율: 0.7541
임곗값: 0.6
오차 행렬
[[112   6]
 [ 16  45]]
정확도: 0.8771, 정밀도: 0.8824, 재현율: 0.7377
In [8]:
from sklearn.metrics import precision_recall_curve

# 레이블 값이 1일때의 예측 확률을 추출 
pred_proba_class1 = lr_clf.predict_proba(X_test)[:, 1] 

# 실제값 데이터 셋과 레이블 값이 1일 때의 예측 확률을 precision_recall_curve 인자로 입력 
precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_class1 )
print('반환된 분류 결정 임곗값 배열의 Shape:', thresholds.shape)
print('반환된 precisions 배열의 Shape:', precisions.shape)
print('반환된 recalls 배열의 Shape:', recalls.shape)

print("thresholds 5 sample:", thresholds[:5])
print("precisions 5 sample:", precisions[:5])
print("recalls 5 sample:", recalls[:5])

#반환된 임계값 배열 로우가 147건이므로 샘플로 10건만 추출하되, 임곗값을 15 Step으로 추출. 
thr_index = np.arange(0, thresholds.shape[0], 15)
print('샘플 추출을 위한 임계값 배열의 index 10개:', thr_index)
print('샘플용 10개의 임곗값: ', np.round(thresholds[thr_index], 2))

# 15 step 단위로 추출된 임계값에 따른 정밀도와 재현율 값 
print('샘플 임계값별 정밀도: ', np.round(precisions[thr_index], 3))
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index], 3))
 
반환된 분류 결정 임곗값 배열의 Shape: (143,)
반환된 precisions 배열의 Shape: (144,)
반환된 recalls 배열의 Shape: (144,)
thresholds 5 sample: [0.10391276 0.103915   0.1039402  0.10782868 0.10888941]
precisions 5 sample: [0.38853503 0.38461538 0.38709677 0.38961039 0.38562092]
recalls 5 sample: [1.         0.98360656 0.98360656 0.98360656 0.96721311]
샘플 추출을 위한 임계값 배열의 index 10개: [  0  15  30  45  60  75  90 105 120 135]
샘플용 10개의 임곗값:  [0.1  0.12 0.14 0.19 0.28 0.4  0.56 0.67 0.82 0.95]
샘플 임계값별 정밀도:  [0.389 0.44  0.466 0.539 0.647 0.729 0.836 0.949 0.958 1.   ]
샘플 임계값별 재현율:  [1.    0.967 0.902 0.902 0.902 0.836 0.754 0.607 0.377 0.148]
In [9]:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%matplotlib inline

def precision_recall_curve_plot(y_test , pred_proba_c1):
    # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출. 
    precisions, recalls, thresholds = precision_recall_curve( y_test, pred_proba_c1)
    
    # X축을 threshold값으로, Y축은 정밀도, 재현율 값으로 각각 Plot 수행. 정밀도는 점선으로 표시
    plt.figure(figsize=(8,6))
    threshold_boundary = thresholds.shape[0]
    plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
    plt.plot(thresholds, recalls[0:threshold_boundary],label='recall')
    
    # threshold 값 X 축의 Scale을 0.1 단위로 변경
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1),2))
    
    # x축, y축 label과 legend, 그리고 grid 설정
    plt.xlabel('Threshold value'); plt.ylabel('Precision and Recall value')
    plt.legend(); plt.grid()
    plt.show()
    
precision_recall_curve_plot( y_test, lr_clf.predict_proba(X_test)[:, 1] )
 
 

F1 Score

In [10]:
from sklearn.metrics import f1_score 
f1 = f1_score(y_test , pred)
print('F1 스코어: {0:.4f}'.format(f1))
 
F1 스코어: 0.7805
 

ROC Curve와 AUC

In [11]:
from sklearn.metrics import roc_curve

# 레이블 값이 1일때의 예측 확률을 추출 
pred_proba_class1 = lr_clf.predict_proba(X_test)[:, 1] 

fprs , tprs , thresholds = roc_curve(y_test, pred_proba_class1)
# 반환된 임곗값 배열 로우가 47건이므로 샘플로 10건만 추출하되, 임곗값을 5 Step으로 추출. 
thr_index = np.arange(0, thresholds.shape[0], 5)
print('샘플 추출을 위한 임곗값 배열의 index 10개:', thr_index)
print('샘플용 10개의 임곗값: ', np.round(thresholds[thr_index], 2))

# 5 step 단위로 추출된 임계값에 따른 FPR, TPR 값
print('샘플 임곗값별 FPR: ', np.round(fprs[thr_index], 3))
print('샘플 임곗값별 TPR: ', np.round(tprs[thr_index], 3))
 
샘플 추출을 위한 임곗값 배열의 index 10개: [ 0  5 10 15 20 25 30 35 40 45 50]
샘플용 10개의 임곗값:  [1.97 0.75 0.63 0.59 0.49 0.4  0.35 0.23 0.13 0.12 0.11]
샘플 임곗값별 FPR:  [0.    0.017 0.034 0.051 0.127 0.161 0.203 0.331 0.585 0.636 0.797]
샘플 임곗값별 TPR:  [0.    0.475 0.689 0.754 0.787 0.836 0.869 0.902 0.918 0.967 0.967]
In [12]:
def roc_curve_plot(y_test , pred_proba_c1):
    # 임곗값에 따른 FPR, TPR 값을 반환 받음. 
    fprs , tprs , thresholds = roc_curve(y_test ,pred_proba_c1)

    # ROC Curve를 plot 곡선으로 그림. 
    plt.plot(fprs , tprs, label='ROC')
    # 가운데 대각선 직선을 그림. 
    plt.plot([0, 1], [0, 1], 'k--', label='Random')
    
    # FPR X 축의 Scale을 0.1 단위로 변경, X,Y 축명 설정등   
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1),2))
    plt.xlim(0,1); plt.ylim(0,1)
    plt.xlabel('FPR( 1 - Sensitivity )'); plt.ylabel('TPR( Recall )')
    plt.legend()
    plt.show()
    
roc_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1] )
 
In [13]:
from sklearn.metrics import roc_auc_score

pred_proba = lr_clf.predict_proba(X_test)[:, 1]
roc_score = roc_auc_score(y_test, pred_proba)
print('ROC AUC 값: {0:.4f}'.format(roc_score))
 
ROC AUC 값: 0.9024
In [ ]: