본문 바로가기

파이썬 머신러닝 완벽가이드

3.1 평가(정밀도/재현율)

728x90

머신러닝은 데이터의 가공/변환, 

모델 학습/예측, 평가로 이루어집니다. 

이번 시간부터는 평가에 대하여 집중적으로 배워보도록 하겠습니다.

우리는 앞서 평가 방법으로 정확도를 사용했습니다. 

이번 장에서는 정확도 이외에 다른 평가방법에 대하여 한번 알아보도록 하겠습니다.

이는 '파이썬 머신러닝 완벽 가이드'를 정리한 내용입니다.


1. 정확도

평가 방법의 첫 번째로는 그동안 우리가 계속 다뤄왔던 정확도가 있습니다. 정확도는 예측과 결과가 동일한 데이터 건수 / 전체 예측 데이터 건수입니다. 

지금까지 정확도로 평가를 진행했습니다. 하지만 정확도가 평가지표로 적절하지 않은 상황이 있습니다. 바로 데이터의 분포도가 불균형할 때입니다. 어떤 상황인지 아래 예시 코드를 통해 알아봅시다.

 

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.base import BaseEstimator
from sklearn.metrics import accuracy_score
import numpy as np
import pandas as pd

 

class MyFakeClassifier(BaseEstimator):
    # fit은 아무것도 수행 안함
    def fit(self,X,y):
        pass
    
    # 무조건 다 0으로 예측
    def predict(self, X):
        return np.zeros( (len(X),1) , dtype=bool)

 

가상의 Classifier을 생성하도록 하겠습니다. 해당 Classifier는 학습은 수행하지 않고 무조건 0으로만 예측하는 모델입니다.

 

# MNIST 데이터 로딩
digits =load_digits()

# y값을 7이면 1로 변환, 7을 제외한 나머지 수는 0으로 변환 
y = (digits.target == 7).astype(int)
X_train, X_test, y_train, y_test = train_test_split( digits.data, y, random_state=11)

# 불균형한 레이블 데이터 분포도 확인. 
print('레이블 테스트 세트 크기 :', y_test.shape)
print('테스트 세트 레이블 0 과 1의 분포도')
print(pd.Series(y_test).value_counts())

 

이제 MINIST 데이터를 불러온 후 7을 제외한 나머지는 모두 0으로 변환하여 불균형한 데이터 레이블을 만들어주었습니다.

 

# Dummy Classifier로 학습/예측/정확도 평가
fakeclf = MyFakeClassifier()
fakeclf.fit(X_train , y_train)
fakepred = fakeclf.predict(X_test)
print('모든 예측을 0으로 하여도 정확도는:{:.3f}'.format(accuracy_score(y_test , fakepred)))

 

레이블 테스트 세트 크기 : (450,)
테스트 세트 레이블 0 과 1의 분포도
0    405
1     45
dtype: int64
모든 예측을 0으로 하여도 정확도는:0.900

 

이제 불균형한 분포도를 가진 레이블을 학습시킨 후 예측을 진행 보도록 하겠습니다. 예측 결과를 보면 정확도가 90%로 매우 높은 값임을 알 수 있습니다. 여기서 바로 정확도를 평가 지표로 사용했을 때 문제점이 드러나게 됩니다.

 

위의 테스트 데이터 레이블 분포도를 보면 0이 압도적으로 많은 것을 볼 수 있습니다. 즉 무조건 그냥 0으로 예측만 하더라도 정확도가 매우 높게 나오는 것이지요. 이러한 문제는 스팸 메일 탐지기나 암 탐지기 같은 불균형한 분포도를 가진 데이터에서 발생할 수 있는 현상입니다. 즉 모델이 아닐 확률이 높기 때문에 자연스럽게 데이터와 상관없이 '아니다'로 학습하는 것이지요.

이러한 데이터에서는 정확도는 적절한 평가지표가 아닙니다.

 

이제 다른 평가 지표를 살펴보도록 하겠습니다.

2. 오차행렬 

다른 평가 지표에 대해 살펴보기 이전에 먼저 오차 행렬에 대해서 알아보도록 하겠습니다. 오차 행렬은 이진 분류의 예측 오류가 얼마인지와 어떠한 유형의 예측 오류가 발생하고 있는지를 함께 나타내는 지표입니다.

 

위의 그림을 통해 알 수 있듯이 오차 행렬은 예측 결과 값과 실제 클래스를 두 축으로 발생 가능한 모든 상황을 표시해줍니다.

앞선 살펴본 불균형한 분포도를 가진 MINIST 데이터를 오차행렬로 표현해보도록 하겠습니다.

 

from sklearn.metrics import confusion_matrix

# y_test와 예측 결과를 오차행렬로 표현
confusion_matrix(y_test, fakepred)

 

array([[405,   0],
       [ 45,   0]])

 

결과는 다음과 같습니다. 오차행렬의 반환 값은 ndarray형태이며 각각의 값은 위의 사진의 값과 대응됩니다.

 

3. 정밀도와 재현율

그럼 이제 새로운 평가 지표인 정밀도와 재현율에 대하여 살펴보도록 하겠습니다.

 

정밀도 : 예측을 Positive로 한 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율

              =TP / (FP +TP)

 

재현율 : 실제값이 Positive인 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율

            =TP / (FN +TP)

 

즉, 정밀도는 예측값재현율은 실제값에 초점을 맞춘 평가지표입니다.

 

정밀도는 실제 Negative인 정상 데이터를 Positive인 비정상 데이터로 판단할 경우 업무상에 문제가 발생하는 상황에서 중요합니다. (정상->비정상) 예를 들어, 정상적인 이메일을 스팸 메일로 처리한다면 아예 메일을 받을 수가 없겠죠?

 

재현율은 실제 Positive 양성 데이터를 Negative로 잘못 판단하게 되면 업무 상에 큰 영향이 발생하는 상황에서 중요합니다. (비정상 -> 정상) 예를 들어, 암을 판단할 때 실제로 암에 걸렸는데 음성이라고 판단하면 아주 큰일 나겠죠?

 

앞선 MNIST 데이터를 정밀도와 재현율의 관점에서 평가하면 0인 것을 볼 수 있습니다.(분자가 모두 0이니깐 당연한 결과이죠?)

 

from sklearn.metrics import accuracy_score, precision_score , recall_score

confusion_matrix(y_test, fakepred)

print("정밀도:", precision_score(y_test, fakepred))
print("재현율:", recall_score(y_test, fakepred))

 

array([[405,   0],
       [ 45,   0]])
정밀도: 0.0
재현율: 0.0

 

4.  정밀도와 재현율의 Trade-off

자! 이제 정밀도와 재현율의 관계에 대하여 더욱 깊이 있게 알아보도록 하겠습니다. 

 

정밀도/재현율의 Trade-off : 정밀도와 재현율은 상호보완적인 평가 지표이다. 임계값 조정을 통해 재현율과 정밀도 중 어느 것을 강조할지 정할 수 있습니다. 

 

정밀도/재현율의 Trade-off에 대해 다루기 이전에 먼저 앞서 실습한 타이타닉 데이터의 재현율과 정밀도를 구해보도록 하겠습니다.

 

from sklearn.metrics import accuracy_score, precision_score , recall_score , confusion_matrix

def get_clf_eval(y_test , pred):
    confusion = confusion_matrix(y_test, pred)
    # 정확도
    accuracy = accuracy_score(y_test , pred)
    # 정밀도
    precision = precision_score(y_test , pred)
    # 재현율
    recall = recall_score(y_test , pred)
    print('오차 행렬')
    print(confusion)
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}'.format(accuracy , precision ,recall))

 

import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split 
from sklearn.linear_model import LogisticRegression

# 원본 데이터를 재로딩, 데이터 가공, 학습데이터/테스트 데이터 분할. 
titanic_df = pd.read_csv('./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)

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_clf_eval(y_test , pred)

 

오차 행렬
[[104  14]
 [ 13  48]]
정확도: 0.8492, 정밀도: 0.7742, 재현율: 0.7869

 

   4.1 결정 확률

정밀도와 재현율의 관계를 이해하기 위해서는 임계값에 대해 알아야 합니다. 임계값을 이해하기 위해 먼저 결정 확률이 무엇인지 알아보도록 하겠습니다.

 

결정확률 : 사이킷런에서는 특정 레이블 값을 구하기 위해 먼저 개별 레이블별 확률을 구하고 그중 가장 높은 값을 선택한다.

예를 들어 이진 분류에서는 0과 1의 각각의 확률을 구하고 1일 확률이 임계값(보통 0.5) 보다 크면 1로 판단한다.

 

이를 실습해 통해 확인해보도록 하겠습니다. 개별 레이블별 확률은 predict_proba()라는 API를 통해 구할 수 있습니다.

pred_proba = lr_clf.predict_proba(X_test)
pred = lr_clf.predict(X_test)

print('pred_proba()결과 Shape : {0}'.format(pred_proba.shape))
pred_proba_result = np.concatenate([pred_proba , pred.reshape(-1,1)],axis=1)
print('두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 \n',pred_proba_result[:3])

 

pred_proba()결과 Shape : (179, 2)
두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 
 [[0.46196457 0.53803543 1.        ]
 [0.87861802 0.12138198 0.        ]
 [0.8771453  0.1228547  0.        ]]

 

위의 결과를 통해 알 수 있듯이 0과 1일 각각의 확률을 구하고 0.5보다 크면 1로 그렇지 않으면 0으로 판단함을 알 수 있습니다.

 

   4.2 임계값과 정밀도/재현율의 Trade-off

앞선 실습을 통해 임계값과 결정 값을 통해 예측값을 구한다는 사실을 알 수 있었습니다. 그렇다면 임계값과 정밀도/재현율의 Trade-off 사이에 어떤 관련성이 있는지 살펴보도록 하겠습니다.(맨 마지막에 있는 그래프를 먼저 참고해주세요!!)

 

아래 실습은 임계값이 0.5, 0.4 일 때 오차 행렬과 정밀도, 재현율의 변화를 보여주는 실습입니다.

from sklearn.preprocessing import Binarizer

#Binarizer의 threshold 설정값. 분류 결정 임곗값임.  
custom_thresholds = [0.5, 0.4]

# Positive 클래스 컬럼 하나만 추출하여 Binarizer를 적용
pred_proba_1 = pred_proba[:,1].reshape(-1,1)

for custom_threshold in custom_thresholds:
  binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1) 
  custom_predict = binarizer.transform(pred_proba_1)
  
  print(f'임계값 : {custom_threshold}')
  get_clf_eval(y_test, custom_predict)

 

임계값 : 0.5
오차 행렬
[[104  14]
 [ 13  48]]
정확도: 0.8492, 정밀도: 0.7742, 재현율: 0.7869
임계값 : 0.4
오차 행렬
[[98 20]
 [10 51]]
정확도: 0.8324, 정밀도: 0.7183, 재현율: 0.8361

 

위의 실습 결과를 보면 알 수 있듯이 임계값이 낮을 때 정밀도는 낮아지고 재현율은 높아짐을 알 수 있습니다. 그 이유는 오차 행렬의 변화를 통해 확인할 수 있습니다. 임계값이 낮아졌다는 것은 1로 판단할 확률 즉, Positive로 예측할 가능성이 더욱 높아졌음을 의미합니다.( 0.5일 때: 62 -> 0.4일 때: 71) 

 

임계값을 낮춘다는 것은 Negative를 Positive로 예측한 경우(FP)가 증가하고, 실제 Positve를 Negative로 예측한 경우(FN) 감소함을 의미합니다. 이에 따라 재현율 = TP / (FN +TP)은 증가하게 되고 정밀도 = TP / (FP +TP)는 감소하는 것이죠!!

 

임계값 변화에 따른 정밀도/재현율의 Trade-off를 그래프를 통해 더욱 직관적으로 확인해보도록 하겠습니다.

from sklearn.metrics import precision_recall_curve

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

precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_class1 )

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

# 15 step 단위로 추출된 임계값에 따른 정밀도와 재현율 값 
print('샘플 임계값별 정밀도: ', np.round(precisions[thr_index], 3))
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index], 3))

 

샘플용 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]

 

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] )


지금까지 정밀도/재현율의 Trade-off에 대하여 살펴봤습니다.

정밀도/재현율은 임계값을 조정하여 한가지 지표를 강조할 수 있습니다.

하지만 특정한 평가지표만을 유독 강조하여 성능을 높였다고(?) 하더라도 

이는 절대로 좋은 알고리즘이라 이야기할 수 없습니다.

정밀도/재현율은 상호보완적인 관계임을 기억해야 합니다.

따라서 다음 시간에는 정밀도/재현율이 조합돼 분류의 종합적인 성능 평가에 사용될 평가 지표에 대해 알아보도록 하겠습니다.

안녕~~~~

사진출처

오차행렬: https://blog.kakaocdn.net/dn/PHe9w/btq8kJXoUBJ/Ni3VOGjFkLKyKIeWmdCxuk/img.png

 

반응형