정밀도와 재현율은 1(=positive) 데이터 세트의 예측 성능에 좀 더 초점을 맞춘 평가 지표다.
•
정밀도 = 예측을 1(양성)로 한 모든 경우 중에 실제로 1(양성)인 경우의 비율
•
재현율 = 실제로 1 인 모든 경우 중에 예측을 1로 한 경우의 비율(=민감도)
재현율이 중요 지표인 경우는 실제 양성인 데이터를 음성으로 잘못 판단하게 되면 큰 문제가 발생하는 경우다. ( 암 판단 모델, 금융 사기 적발 모델 등등)
보통은 재현율(민감도)이 정밀도보다 상대적으로 중요할 경우가 더 많다.
하지만, 스팸 메일 여부를 판단하는 모델 같은 경우에는 실제 음성인(일반인) 메일을 양성(스팸인)메일로 분류해버려서 받지 못하는 문제를 더 심각하게 판단하기 때문에 이런 경우에는 음성의 정밀도가 중요하다고 생각할 수 있다.
어쨌든 재현율과 정밀도는 서로 보완적인 관계이다. (정밀도/재현율 트레이드오프) 최적의 성능은 재현율과 정밀도가 모두 높은 결과다. 이 중 한 평가만 높고 한 평가는 매우 낮게 되면 좋은 성능은 아니다.
이제, 타이타닉 예제를 가지고 오차 행렬, 정밀도, 재현율을 통한 예측 성능을 평가해보자.
사이킷런에서는 정밀도는 precision_score(), 재현율은 recall_score()을 API로 제공한다.
평가를 간편하게 하기위해서 평가를 한꺼번에 호출하는 get_clf_eval() 함수를 만들어보겠다. 그리고 로지스틱 회귀를 사용해서 분류를 해보겠다.
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))
Python
복사
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.2,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
'''
Python
복사
결과를 확인해보면 정확도보다 정밀도가 낮게 나왔다. 재현율과 정밀도를 높이려면 어떻게 해야할까?
정밀도/재현율 트레이드오프
정밀도와 재현율의 한쪽이 상대적으로 더 중요할 경우, 결정 임계값을 조정해 정밀도와 재현율의 수치를 높일 수 있다. 하지만 둘의 관계는 상호보완적이기 때문에 하나의 값을 높이면 다른 하나의 값은 낮아지게 된다. 이것을 정밀도/재현율의 트레이드오프라고 부른다.
일반적으로 이진 분류에서는 기본으로 임계값을 0.5로 설정한다.
임곗값을 높일수록 재현율(민감도)은 낮아지고 반대로 정밀도는 높아진다.
사이킷런은 개별 데이터별 예측 확률을 반환하는 메서드 predict_proba() 를 제공한다. 학습이 완료된 분류 알고리즘 객체에서 호출이 가능하고 파라미터로 테스트 피처 데이터를 입력해주면 테스트 데이터의 예측 확률을 반환해준다.
앞 예제의 타이타닉 생존자 데이터를 학습한 LogisticRegression 객체에서 predict_proba() 메서드를 수행한 뒤 반환 값을 확인하고 predict() 메서드와 비교해보자.
pred_proba = lr_clf.predict_proba(X_test)
pred = lr_clf.predict(X_test)
print('predict_proba() 결과 shape: {0}'.format(pred_proba.shape))
'''
predict_proba() 결과 shape: (179, 2)
'''
print('predict_proba array에서 앞 3개만 샘플로 추출 \n:', pred_proba[:3])
'''
predict_proba array에서 앞 3개만 샘플로 추출
: [[0.46175211 0.53824789]
[0.87863924 0.12136076]
[0.87717092 0.12282908]]
'''
# 예측 확률 array와 예측 결과값 array를 병합해 예측 확률과 결과값을 한눈에 확인
pred_proba_result = np.concatenate([pred_proba,pred.reshape(-1,1)], axis=1) # 열 병합
print('두 개의 class 중에서 더 큰 확률을 클래스 값으로 예측 \n', pred_proba_result[:3])
'''
두 개의 class 중에서 더 큰 확률을 클래스 값으로 예측
[[0.46175211 0.53824789 1. ]
[0.87863924 0.12136076 0. ]
[0.87717092 0.12282908 0. ]]
'''
Python
복사
이제부터, 분류 결정 임계값을 조절해 정밀도와 재현율의 성능 수치를 조절하는 코드를 직접 구현하면서 이해해 보겠다.
사이킷런의 Binarizer 클래스를 이용해서 임계값을 활용하는 방법을 알아본다. 임계값 변수를 특정 값으로 설정하고 Binarizer클래스를 객체로 생성한다.
Binarizer 객체의 fit_transform()메서드를 이용하여 넘파이 배열을 입력하면 임계값보다 작거나 같으면 0으로, 크면 1값으로 변환해 반환한다.
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))
'''
[[0. 0. 1.]
[1. 0. 0.]
[0. 0. 1.]]
'''
Python
복사
위의 방식을 이용해서 앞의 예제에서의 predict_proba() 메서드를 직접 구현해보자.
from sklearn.preprocessing import Binarizer
# Binarizer의 임계값 0.5로 설정
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)
'''
오차 행렬
[[104 14]
[ 13 48]]
정확도:0.8492, 정밀도:0.7742, 재현율:0.7869
'''
Python
복사
결과는 앞의 predict()메서드의 결과반환값과 같다.
만약, 결정 임계값을 낮추면 어떤 결과가 나올까? 임계값을 0.4로 낮춰보자. 상식적으로 임계값을 낮추면 1이 될 확률이 높아지기 때문에 양의 값의 확률이 높아지게 된다. 즉, 재현율의 값은 높아지게 된다. 양성을 음성으로 예측하는 경우가 줄어들기 때문이다. 하지만, 정밀도와 정확도는 상대적으로 떨어지게 될 것이다. 그만큼 음성인데도 불구하고 양성으로 예측하는 오류가 많아질 것이기 때문이다.
# Binarizer 의 threshold 설정값을 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)
'''
오차 행렬
[[98 20]
[10 51]]
정확도:0.8324, 정밀도:0.7183, 재현율:0.8361
'''
Python
복사
결과에도 알 수 있듯이 재현율 값은 올라가고 정밀도는 떨어졌다.
이제부터는 임계값을 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)
'''
임계값: 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
'''
Python
복사
임계값 변화에 따른 평가 지표 값을 알아보는 코드는 사이킷런의 precision_recall_curve() API에서 제공한다. 일반적으로 0.11 ~ 0.95 정도의 임계값을 담은 넘파이 배열을 활용한다.
•
입력 파라미터 :
◦
y_ture : 실제 클래스값 배열
◦
probas_pred : Positive 칼럼의 예측 확률 배열
•
반환 값 :
◦
정밀도 : 임계값별 정밀도 값을 배열로 반환
◦
재현율 : 임계값별 재현율 값을 배열로 반환
임계값이 너무 작은 값 단위로 많이 구성되어있기 때문에 15단계로 추출해 살펴보겠다.
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)
'''
반환된 분류 결정 임계값 배열의 shape: (143,)
'''
# 반환된 임계값 배열 로우가 143건이므로 샘플로 10 건만 추출하되, 임계값을 15 step으로 추출
thr_index = np.arange(0,thresholds.shape[0],15)
print('샘플 추출을 위한 임계값 배열의 index 10개:', thr_index)
'''
샘플 추출을 위한 임계값 배열의 index 10개: [ 0 15 30 45 60 75 90 105 120 135]
'''
print('샘플용 10개의 임계값:', np.round(thresholds[thr_index],2))
'''
샘플용 10개의 임계값: [0.1 0.12 0.14 0.19 0.28 0.4 0.56 0.67 0.82 0.95]
'''
# 15 step 단위로 추출된 임계값에 따른 정밀도와 재현율 값
print('샘플 임계값별 정밀도: ', np.round(precisions[thr_index],3))
'''
샘플 임계값별 정밀도: [0.389 0.44 0.466 0.539 0.647 0.729 0.836 0.949 0.958 1. ]
정밀도는 임계값이 높아질수록 크게 된다.
'''
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index],3))
'''
샘플 임계값별 재현율: [1. 0.967 0.902 0.902 0.902 0.836 0.754 0.607 0.377 0.148]
'''
# 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):
# 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])
Python
복사