파이썬 머신러닝 완벽 가이드_ch3 평가_정밀도와 재현율(2)

생계발자·2024년 4월 1일

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

1. 정밀도/재현율 트레이드 오프

분류하려는 업무 특성상 정밀도 또는 재현율이 특별히 강조돼야할 경우 분류의 결정 임계값(Thershold)을 조정해 정밀도 또는 재현율을 높일 수 있다.

정밀도/재현율의 트레이드오프(Trade-off)

하지만 정밀도와 재현율은 상호보완적인 평가 지표이기 때문에 어느 한 쪽을 강제로 높이면 다른 하나의 수치는 떨어지기 쉽다.

사이킷런은 개별 데이터별로 예측 확률을 반환하는 메서드인 predict_proba()를 제공

입력 파라미터

  • predict() 메서드와 동일하게 보통 테스트 피처 데이터 세트를 입력

반환값

  • 개별 클래스의 예측 확률을 ndarray m x n (m: 입력값의 레코드 수, n: 클래스 값 유형) 형태로 반환

각 열은 개별 클래스의 예측 확률, 이진 분류에서 첫 번쨰 칼럼은 0 Negative의 확률, 두 번째 칼럼은 1 Positive의 확률

타이타닉 예제에서의 predict_proba() 메서드를 수행한 뒤 반환 값을 확인하고 predict() 메서드와 비교해보자

pred_proba = Ir_clf.predict_proba(X_test)
pred = Ir_clf.predict(X_test)
print('pred_proba()결과 shape:{0}'.format(pred_proba.shape))
print('pred_proba array에서 앞 3개만 샘플로 추출 \n:', pred_proba[:3])

#예측 확률 array와 예측 결과값 array를 병합하여(concatenate) 예측 확률과 결괏값을 한눈에 확인
pred_proba_result = np.concatenate([pred_proba, pred.reshape(-1,1)], axis=1)
print('두 개의 class 중에서 더 큰 확률을 클래스 값으로 예측 \n', pred_proba_result[:3])


[output]
pred_proba()결과 shape:(179, 2)
pred_proba array에서 앞 3개만 샘플로 추출 
: [[0.44935227 0.55064773]
 [0.86335512 0.13664488]
 [0.86429645 0.13570355]]
두 개의 class 중에서 더 큰 확률을 클래스 값으로 예측 
 [[0.44935227 0.55064773 1.        ]
 [0.86335512 0.13664488 0.        ]
 [0.86429645 0.13570355 0.        ]]

반환 결과인 ndarray는 0과 1에 대한 확률을 나타내므로 첫 번째 칼럼값과 두 번째 칼럼값을 더하면 1이 된다.

또한 predict() 메서드 결과 비교에서도 나타나듯이, 두 개의 칼럼 중에서 더 큰 확률 값으로 predict()가 최종 예측하고 있다.

사이킷런의 predict()는 predict_proba() 메서드가 반환하는 확률 값을 가진 ndarray에서 정해진 임계값을 만족하는 ndarray의 칼럼 위치를 최종 예측 클래스로 결정한다.

이러한 로직을 사이킷런의 Binarizer 클래스를 이용해 코드로 구현하여 정밀도/재현율 트레이드 오프 방식을 이해해보자

먼저 Binarizer 클래스의 사용법부터 간단히 알아보자

from sklearn.preprocessing import Binarizer

X= [[ 1, -1, 2],
    [ 2, 0, 0],
   [0, 1.1, 1.2]]

# X의 개별 원소들이 threshold 값보다 같거나 작으면 0, 크면 1을 반환
binarizer = Binarizer(threshold=1.1)
print(binarizer.fit_transform(X))


[output]
[[0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]]

이제 이 Binarizer를 이용해 사이킷런 predict()의 의사(pseudo)코드를 만들어보자

from sklearn.preprocessing import Binarizer

#Binarizer의 threshold 설정값. 분류 결정 임계값임
custom_threshold = 0.5

#predict_proba() 반환값의 두 번째 칼럼, 즉 Positive 클래스 칼럼 하나만 추출해 Binarizer를 적용
pred_proba_1 = pred_proba[:, 1].reshape(-1, 1)

binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1)
custom_predict = binarizer.transform(pred_proba_1)

get_clf_eval(y_test, custom_predict)


[output]
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도:0.8246, 재현율:0.7705

이 의사코드는 앞에서 predict()로 계산된 지표 값과 정확히 같다. 즉, predict()가 predict_proba()에 기반함을 알 수 있다.

#Binarizer의 threshold 설정값을 0.5에서 0.4로 낮춤. 
custom_threshold = 0.4
pred_proba_1 = pred_proba[:, 1].reshape(-1, 1)
binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1)
custom_predict = binarizer.transform(pred_proba_1)

get_clf_eval(y_test, custom_predict)


[output]
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도:0.7042, 재현율:0.8197

임계값을 낮추니 정밀도는 떨어지고 재현율이 올라간 것을 확인

그 이유는 임계값은 Positive 예측값을 결정하는 확률의 기준이기 때문에 이를 낮추면 Positive 예측을 할 확률에 대한 범위가 더 넓어진다.

그러므로 양성 예측을 많이하게 되어 실제 양성을 음성으로 예측하는 횟수가 상대적으로 줄어들게된다.


이번에는 임계값을 0.4부터 0.6까지 0.05씩 증가시키며 평가 지표를 조사해보자

이를 위해 get_eval_by_threshold() 생성

#테스트를 수행할 모든 임곗값을 리스트 객체로 저장.
thresholds = [0.4, 0.45, 0.5, 0.55, 0.6]

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_clf_eval(y_test, custom_predict)
        
get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1,1), thresholds)



[output]
임곗값: 0.4
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도:0.7042, 재현율:0.8197
임곗값: 0.45
오차 행렬
[[105  13]
 [ 13  48]]
정확도: 0.8547, 정밀도:0.7869, 재현율:0.7869
임곗값: 0.5
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도:0.8246, 재현율:0.7705
임곗값: 0.55
오차 행렬
[[111   7]
 [ 16  45]]
정확도: 0.8715, 정밀도:0.8654, 재현율:0.7377
임곗값: 0.6
오차 행렬
[[113   5]
 [ 17  44]]
정확도: 0.8771, 정밀도:0.8980, 재현율:0.7213

임곗값이 0.45일 경우에 디폴트 0.5인 경우와 비교해서 정확도는 동일하고 정밀도는 약간 떨어졌으나 재현율이 오른 것을 확인할 수 있다.

사이킷런은 이와 유사한 precision_recall_curve() API를 제공

입력 파라미터:

  • y_true: 실제 클래스값 배열(배열 크기 = [데이터 건수])
  • probas_pred: Positive 칼럼의 예측 확률 배열(배열 크기=[데이터 건수])

반환값:

  • 정밀도: 임곗값별 정밀도 값을 배열로 반환
  • 재현율: 임곗값별 재현율 값을 배열로 반환

이 메서드를 이용해 타이타닉 예측 모델의 임곗값별 정밀도와 재현율을 구해보자

from sklearn.metrics import precision_recall_curve

#레이블 값이 1일때 예측확률을 추출
pred_proba_class1 = Ir_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)

#반환된 임계값 배열 로우가 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))


[output]
반환된 분류 결정 임곗값 배열의 shape: (147,)
샘플 추출을 위한 임계값 배열의 index 10개: [  0  15  30  45  60  75  90 105 120 135]
샘플용 10개의 임곗값: [0.12 0.13 0.15 0.17 0.26 0.38 0.49 0.63 0.76 0.9 ]
샘플 임계값별 정밀도: [0.379 0.424 0.455 0.519 0.618 0.676 0.797 0.93  0.964 1.   ]
샘플 임계값별 재현율: [1.    0.967 0.902 0.902 0.902 0.82  0.77  0.656 0.443 0.213]

추출된 임곗값 샘플 10개에 해당하는 정밀도 값과 재현율 값을 살펴보면 임곗값이 증가할수록 정밀도 값은 동시에 높아지나 재현율 값은 낮아짐을 알 수 있다.

precision_recall_curve() API는 정밀도와 재현율의 임곗값에 따른 값 변화를 곡선 형태의 그래프로 시각화 하는 데 이용할 수 있다.

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%matplotlib inline

def precision_recall_curve_plot(y_test, pred_proba_c1):
    # thredshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출 
    precision, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1)
    
    #X축을 thredshold 값으로, Y축은 정밀도, 재현율 값으로 각각 plot 수행. 정밀도는 점선으로 표시 
    plt.figure(figsize=(8,6))
    threshold_boundary = thresholds.shape[0]
    plt.plot(thresholds, precision[0:threshold_boundary], linestyle='--', label='precision')
    plt.plot(thresholds, recalls[0:threshold_boundary], label ='recall')
    
    #thredshold 값 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, Ir_clf.predict_proba(X_test)[:,1])

[output]

2. 정밀도와 재현율의 쟁점

[정밀도가 100%가 되는 방법]

확실한 기준이 되는 경우만 Positive로 예측하고 나머지는 모두 Negative로 예측한다.

[재현율이 100%가 되는 방법]

모든 암 환자를 Positive로 예측하면 된다.

이처럼 정밀도와 재현율 성능 수치도 어느 한 쪽만 참조하면 극단적인 수치 조작이 가능하다.

따라서 정밀도 또는 재현율 중 하나만 스코어가 좋고 다른 하나는 스코어가 나쁜 분류는 성능이 좋지 않다고 할 수 있다.

profile
생(계를 위한) 개발자

0개의 댓글