배운 내용
- BaseEstimator
- load_digits
- confusion_matrix()
- precision_score()
- recall_score()
- LogisticRegression
- predict_proba()
- np.concatenate()
- Binarizer.fit_transform()
- precision_recall_curve()
- f1_score()
- roc_curve()
머신러닝은 데이터 가공/변환, 모델 학습/예측, 그리고 평가(Evaluation)의 프로세스로 구성됩니다. 모델 예측 성능의 평가를 위해 지금까지 정확도(Accuracy)를 이용해왔습니다. 머신러닝 모델은 여러 가지 방법으로 예측 성능을 평가할 수 있습니다. 성능 평가 지표(Evaluation Metric)는 보통 모델이 회귀냐 분류이냐에 따라 여러 종류로 나뉩니다.
회귀의 경우 대부분 실제값과 예측값의 오차 평균값에 기반합니다. 즉, 오차를 가지고 정규화 수준을 재가공하는 방법이 회귀의 성능 평가 지표 유형입니다.
분류의 경우도 보통 실제 결과 데이터와 예측 결과 데이터가 얼마나 정확하고 오류가 적게 발생하는가에 기반합니다. 하지만, 단순히 이러한 정확도만 가지고 판단했다가는 잘못된 평가 결과에 빠질 수 있습니다. 이 장에서는 결정값이 0과 1로 한정되는 이진 분류의 성능 평가 지표에 대해서 집중적으로 설명하겠습니다. 이진 분류의 경우 정확도보다 다른 성능 평가 지표가 더 중요시되는 경우가 많습니다.
분류
분류의 성능 평가 지표부터 살펴보겠습니다.
- 정확도(Accuracy)
- 오차행렬(Confusion Matrix)
- 정밀도(Precision)
- 재현율(Recall)
- F1 스코어
- ROC AUC
분류는 이진 분류와 여러개의 결정 클래스 값을 거지는 멀티 분류로 나눌 수 있습니다. 위에서 언급한 분류의 성능 지표는 이진/멀티 모두 적용되는 지표이지만, 특히 이진 분류에서 더욱 강조하는 지표입니다.
정확도(Accuracy)
정확도는 직관적으로 모델 예측 성능을 나타내는 평가 지표입니다. 하지만, 이진 분류의 경우 데이터의 구성에 따라 ML의 성능을 왜곡할 수 있어서 정확도만 가지고 성능을 평가하지 않습니다. 정확도가 어떻게 성능을 왜곡하는지 예제로 살펴보겠습니다.
타이타닉 예제를 살펴보면, ML 알고리즘을 적용한 후 예측 정확도 결과가 보통 80%였습니다. 주의 깊게 생각해보면, 생존확률이 남자보다 여자가 높았기때문에 남자는 사망, 여자는 생존으로 예측 결과를 예측해도 비슷한 수치가 나올 수도 있습니다. 단지 성별 조건 하나만 가지고 결정하는 알고리즘도 높은 정확도를 나타내는 생황이 발생하는 것입니다.
BaseEstimator
BaseEstimator 클래스를 상속받으면, Customized 형태의 Estimator를 개발자가 생성할 수 있습니다. 우리가 생성할 MyDummyClassifier 클래스는 학습을 수행하는 fit() 메서드는 아무것도 수행하지 않으며 예측을 수행하는 predict() 메서드는 단순히 Sex 피처가 1이면 0, 그렇지 않으면 1로 예측하는 간단한 Classifier입니다.
from sklearn.base import BaseEstimator
class MyDummyClassifier(BaseEstimator):
# fit() 메서드는 아무 것도 학습하지 않음
## self는 인스턴스 객체를 가르키고, 메서드 사용시 입력하지 않아도 된다.
def fit(self, X, y=None):
pass
# predict() 메서드는 단순히 Sex 피처가 1이면 0, 그렇지 않으면 1로 예측함
def predict(self, X):
pred = np.zeros((X.shape[0], 1))
for i in range (X.shape[0]) :
if X['Sex'].iloc[i] == 1:
pred[i] = 0
else:
pred[i] = 1
return pred
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 원본 데이터를 재로딩, 데이터 가공, 학습 데이터/테스트 데이터 분할
titanic_df = pd.read_csv('/content/drive/MyDrive/military/train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df = titanic_df.drop(['Survived'], axis=1)
#transform_features는 타이타닉 예제 글에 있습니다.
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=0)
myclf = MyDummyClassifier()
myclf.fit(X_train, y_train)
mypredictions = myclf.predict(X_test)
print('Dummy Classifier의 정확도는: {0:.4f}'.format(accuracy_score(y_test, mypredictions)))
'''
Dummy Classifier의 정확도는: 0.7877
'''
위 처럼 간단한 알고리즘으로 예측하더라도, 데이터의 구성에 따라 정확도가 78.77%로 나올 수 있습니다. 따라서 정확도를 평가 지표로 사용할 때는 매우 신중해야 합니다. 특히, 정확도는 imbalanced(불균형한) 레이블 값 분포에서 ML 모델의 성능을 판단할 때 적합한 평가 지표가 아닙니다.
가령 100개의 데이터가 있고, 이 중 90개의 데이터 레이블이 0, 단 10개의 데이터 레이블이 1이라고 한다면 무조건 0으로 예측 결과를 반환하는 ML 모델의 경우라도 정확도가 90%이 됩니다.
MNIST 예제
MNIST 데이터 세트를 변환해 불균형한 데이터 세트로 만든 뒤에 정확도 지표 적용 시 어떤 문제가 발생할 수 있는지 살펴보겠습니다. 사이킷런에서는 load_digits() API를 통해 MNIST 데이터 세트를 제공합니다. MNIST 데이터 세트는 0부터 9까지의 숫자 이미지의 픽셀 정보를 가지고 있으며, 이를 기반으로 숫자 Digit을 예측하는데 사용됩니다. 여기서는 레이블 값이 7만 True이고 나머지 숫자는 False인 이진 분류 문제로 살짝 바꿀 것입니다.
이번에는 아무것도 하지 않고 무조건 특정한 결과로 찍어도 데이터 분포도가 균일하지 않은 경우 높은 수치가 나타날 수 있다는 것을 보이겠습니다.
MyFakeClassifier의 predict함수에서 np.zeros((len(X), 1), dtype=bool)에서 X는 숫자이미지의 픽셀값을 ndarray 형태로 담고 있다. X_test에 총 450개의 ndarray 세트가 있으므로, 반환되는 ndarray는 (450, 1)이다.
train_test_split에 대입하는 y값은 digits.target에 레이블 값을 7과 동일한지 여부에 따라 0과 1로 바꾼 값이 list형으로 저장돼있다. 이를 전체 label로 대입해 이진 분류 문제로 바꾼 것이다.
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):
def fit(self, X, y):
pass
# 입력값으로 들어오는 X 데이터 세트의 크기만큼 모두 0값으로 만들어서 반환
def predict(self, X):
return np.zeros((len(X), 1), dtype=bool)
# 사이킷런의 내장 데이터 세트인 load_digits()를 이용해 MNIST 데이터 로딩
digits = load_digits()
# digits 번호가 7이면 True이고 이를 astype(int)로 1로 변환, 7번이 아니면 False이고 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())
# 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
'''
단순히 predict()의 결과를 np.zeros()로 모두 0값으로 반환함에도 불구하고 450개의 테스트 데이터 세트에 수행한 예측 정확도는 90%입니다. 단지 0으로만 예측해도 정확도가 90%로 유수의 ML 알고리즘과 어깨를 겨룰 수 있다는 것은 말도 안되는 결과입니다.
정확도가 가지는 분류 평가 지표로서 이러한 한계점을 극복하기 위해 여러 가지 분류 지표와 함께 사용해야 합니다. 먼저 True/False, Positive/Negative의 4분면으로 구성되는 오차 행렬(Confusion Matrix)에 대해 설명하겠습니다.
오차 행렬
이진 분류에서 오차행렬(confusion matrix, 혼동행렬)은 학습된 분류 모델이 예측을 수행하면서 얼마나 헷갈리고(confused) 있는지도 함께 보여주는 지표입니다. 즉, 이진 분류의 예측 오류가 얼마인지와 더불어 어떠한 유형의 예측 오류가 발생하고 있는지를 함께 나타내는 지표입니다.
오차행렬은 4분면 행렬에서 실제 클레스 값과, 예측 클레스 값의 일치 여부에 따라 TN, FP, FN, TP 형태로 4분면을 채울 수 있습니다. 앞의 T는 실제 값과 예측값이 같다는 의미인 True, F는 다르다는 의미인 F입니다. 뒤의 N은 예측값인 Negative(0)과 Positive(1)을 나타냅니다.
confusion_matrix()
사이킷런은 오차 행렬을 구하기 위해 confusion_matrix() API를 제공합니다. MyFakeClassifier의 예측 결과인 fakepred와 실제 결과인 y_test를 confusion_matrix()의 인자로 입력해 오차 행렬을 confusion_matrix()를 이용해 배열 형태로 출력합니다.
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, fakepred)
'''# TN FP
array([[405, 0],
[ 45, 0]])
'''# FN TP
TN이 405개, FN이 45개로 오차행렬이 반환됨을 알 수 있습니다. TN, FP, FN, TP 값을 통해 Classifier의 정확도(Accuracy), 정밀도(Precision), 재현율(Recall) 값들을 알 수 있습니다. 예를 들어 정확도는 아래의 수식으로 정의될 수 있습니다.
중요하지만, 매우 적은 수의 결과값에 positive(1)을 부여하는 경우라고 가정해보겠습니다. 가령, 금융 사기가 발생한 사건을 positive(1)라고 하고, 그렇지 않은 사건을 negative(0)라 할때, 압도적으로 학습 데이터의 레이블값에 negative가 많기 때문에 예측 결과로 TN이 매우 커지고 TP가 매우 적게 나타납니다.
또한 negative로 예측할 때 정확도가 높기 때문에 FN이 매우 작고 Positive로 예측할 경우도 작기 때문에 FP도 적게 나타납니다. 이렇게 imbalanced한 데이터 세트에서 Classifiaction 모델의 신뢰도가 떨어지는 사례를 봤습니다.
정밀도와 재현율
정밀도와 재현율은 Positive 데이터 세트의 예측 성능에 좀 더 초점을 마춘 평가 지표 입니다. 앞서 만든 MyFakeClassifier는 Positive로 예측한 TP 값이 하나도 없기 때문에 정밀도와 재현율 값이 모두 0입니다.
정밀도의 분모인 FP + TP는 예측을 Positive로 한 모든 데이터 건수이며, 분자인 TP는 예측과 실제값이 Positive로 일치한 데이터 건수입니다. Positive 예측 성능을 더욱 정밀하게 측정하기 위한 평가 지표로 양성 예측도라고도 불립니다. 사이킷런에서 precision_score() API를 제공합니다.
재현율의 분모인 FN + TP는 실제값이 Positive인 모든 데이터 건수이며, 분자인 TP는 예측과 실제값이 Positive로 일치한 데이터 건수입니다. 민감도(Sensitivity) 또는 TPR(True Positive Rate)라고도 불립니다. 사이킷런에서 recall_score() API를 제공합니다
정밀도와 재현율은 이진 분류 모델의 업무 특성에 따라서 특정 평가 지표가 더 중요한 지표로 간주될 수 있습니다. 또한, 정밀도와 재현율은 서로 보완적인 관계이기 때문에 분류의 성능을 평가하는데 함께 자주 사용됩니다.
재현율이 중요 지표인 경우는 실제 Positive 양성 데이터를 Negative로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우입니다. 예를 들어 암 판단 모델은 재현율이 훨씬 중요한 지표입니다. 왜냐하면 실제 Positive인 암 환자를 Positive 양성이 아닌 Negetive 음성으로 잘못 판단했을 경우 오류의 대가가 생명을 앗아갈 정도로 심각하기 때문입니다.
보통 재현율이 정밀도보다 중요한 지표이지만, 정밀도가 중요한 지표인 경우도 있습니다. 이 경우는 실제 Negative 음성 데이터 예측을 Positive 양성으로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우입니다. 예를 들면 스팸메일 여부를 판단하는 모델의 경우 실제 Positive인 스펨 메일을 Negative인 일반 메일로 분류하더라도 큰 문제가 발생하지 않지만, 실제 Negative인 일반 메일을 Positive인 스팸 메일로 분류할 경우에는 메일을 받지 못해 업무에 차질이 생깁니다.
타이타닉 예제에서 정밀도, 재현도 구하기
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))
from sklearn.linear_model import LogisticRegression
# 원본 데이터를 재로딩, 데이터 가공, 학습 데이터/테스트 데이터 분할
titanic_df = pd.read_csv('/content/drive/MyDrive/military/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=4)
lr_clf = LogisticRegression()
lr_clf.fit(X_train, y_train)
pred = lr_clf.predict(X_test)
get_clf_eval(y_test,pred)
'''
오차 행렬
[[109 10]
[ 18 42]]
정확도: 0.8436, 정밀도: 0.8077, 재현율: 0.7000
'''
Precision(정밀도)에 비해 Recall(재현율)이 낮게 나왔습니다. 재현율 또는 정밀도를 좀 더 강화할 방법을 알아봅시다.
정밀도/재현율 트레이드오프
분류하려는 업무의 특성상 정밀도 또는 재현율이 특별히 강조돼야할 경우 분류의 결정 임계값(Threshold)을 조정해 정밀도 또는 재현율의 수치를 높힐 수 있습니다. 하지만, 정밀도와 재현율은 상호 보완적인 평가 지표이기 때문에 어느 한쪽을 강제로 높히면 다른 하나의 수치는 떨어지기 쉽습니다. 이를 정밀도/재현율 트레이드오프(Trade-off)라고 부릅니다.
사이킷런의 분류 알고리즘은 예측 데이터가 특정 레이블(Label, 결정 클래스 값)에 속하는지 계산하기 위해 먼저 각각 개별 레이블 마다 결정 확률을 구합니다. 그리고 예측 확률이 큰 레이블값으로 예측하게 됩니다. 가령 이진 분류 모델에서 특정 데이터가 0이 될 확률이 10%, 1이 될 확률이 90%로 예측됐다면 최종 예측은 더 큰 확률을 가진 1로 예측합니다. 일반적으로 이진 분류에서는 이 임곗값을 0.5, 즉 50%로 정하고 이 기준값보다 확률이 크면 Positive, 작으면 Negative로 결정합니다.
사이킷런은 개별 데이터별로 예측 확률을 반환하는 메서드인 predict_proba()를 제공합니다. predict_proba() 메서드는 학습이 완료된 사이킷런 Classifier 객체에서 호출이 가능하며 테스트 피처 데이터 세트를 파라미터로 입력해주면 테스트 피처 레코드의 개별 클래스 예측 확률을 반환합니다. predict() 메서드와 유사하지만 단지 반환 결과가 예측 결과 클래스값이 아닌 예측 확률 결과입니다.
predict_proba() 메서드의 경우 개별 클래스의 예측 확률을 ndarray m x n (m: 입력값의 레코드 수, n: 클래스 값 유형) 형태로 반환합니다. 입력 테스트 데이터 세트의 표본 개수가 100개이고 예측 클래스 값 유형이 2개(이진 분류)라면 반환값은 100 x 2 ndarray입니다.
각 열은 개별 클래스의 예측 확률입니다. 이진 분류에서 첫 번째 칼럼은 0 Negative의 확률, 두 번째 칼럼은 1 Positive의 확률입니다.
사실 predict()는 predict_proba() 메서드에 기반해 생성된 API입니다. predict()는 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))
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])
'''
pred_proba()결과 Shape : (179, 2)
pred_proba array에서 앞 3개만 샘플로 추출
: [[0.85081319 0.14918681]
[0.85504 0.14496 ]
[0.36536576 0.63463424]]
두 개의 class 중에서 더 큰 확률을 클래스 값으로 예측
[[0.85081319 0.14918681 0. ]
[0.85504 0.14496 0. ]
[0.36536576 0.63463424 1. ]]
'''
Binarizer.fit_transform()
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.]]
'''
Binarizer.fit_transform() 메서드를 이용해 ndarray를 입력하면, 입력된 ndarray의 값을 지정된 threshold보다 같거나 작으면 0값으로, 크면 1값으로 변환해 반환합니다.
Binarizer를 이용해 사이킷런 predict()의 의사(pseudo) 코드를 만들어 보겠습니다. 앞 예제의 LogisticRegression 객체의 predict_proba() 메서드로 구한 각 클래스별 예측 확률값인 pred_proba 객체 변수에 분류 결정 임곗값(threshold)을 0.5로 지정한 Binarizer 클래스를 적용해 최종 예측값을 구하는 방식입니다.
pred_proba = lr_clf.predict_proba(X_test)
pred = lr_clf.predict(X_test)
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)
'''
오차 행렬
[[109 10]
[ 18 42]]
정확도: 0.8436, 정밀도: 0.8077, 재현율: 0.7000
'''
단지 위에서는 fit_transform()을 썼다면, 여기선 fit과 transform을 분리해서 사용한 것 뿐이다. 앞 예제의 타이타닉 데이터로 학습된 로지스틱 회귀 Classifier 객체에서 호출된 predict()로 계산된 지표값과 정확히 같습니다. predict()가 predict_proba()에 기반함을 알 수 있습니다.
분류 결정 임곗값을 낮추면 평가 지표가 어떻게 변할까요? 임곗값을 0.4로 낮춰보겠습니다.
# Binarizer의 threshold 설정값. 분류 결정 임곗값을 0.4로 낮춤
custom_threshold = 0.4
...
get_clf_eval(y_test, custom_predict)
'''
오차 행렬
[[104 15]
[ 16 44]]
정확도: 0.8268, 정밀도: 0.7458, 재현율: 0.7333
'''
임곗값을 낮추니 재현율 값이 올라가고 정밀도가 떨어졌습니다. 이유는 분류 결정 임곗값은 Positive 예측값을 결정하는 확률의 기준이 되기 때문입니다. 확률이 0.4 초과부터 Positive로 예측을 더 너그럽게 하기 때문에 임계값을 낮출수록 True 값이 많아지게 됩니다.
임곗값을 0.4로 낮춤으로써 TP가 42에서 44로 늘고, FN이 18에서 16으로 줄었습니다. 따라서 재현율이 좋아졌습니다.
하지만, FP는 10에서 15로 늘면서 정밀도가 0.8에서 0.74로 많이 낮아지고 정확도도 0.84에서 0.82로 나빠졌습니다.
임계값을 0.4에서 0.6까지 0.05씩 증가시키며 평가 지표를 조사하겠습니다. 이를 위해 get_eval_by_threshold()함수를 만들겠습니다.
# 테스트를 수행할 모든 임곗값을 리스트 객체로 저장
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_clf_eval(y_test, custom_predict)
get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1, 1), thresholds)
'''
임계값: 0.4
정확도: 0.8268, 정밀도: 0.7458, 재현율: 0.7333
임계값: 0.45
정확도: 0.8212, 정밀도: 0.7414, 재현율: 0.7167
임계값: 0.5
정확도: 0.8436, 정밀도: 0.8077, 재현율: 0.7000
임계값: 0.55
정확도: 0.8380, 정밀도: 0.8298, 재현율: 0.6500
임계값: 0.6
정확도: 0.8268, 정밀도: 0.8222, 재현율: 0.6167
'''
정밀도와 재현율을 함께 비교해보니 임계값이 0.4인 것이 가장 적당해 보입니다. 정밀도와 재현율이 비슷한 수준으로 높고, 정확도도 다른 임계값에 비해 크게 작지 않기 때문입니다.
precision_recall_curve()
사이킷런은 앞의 코드와 유사한 기능을 하는 precision_recall_curve() API를 제공합니다. precision_recall_value()는 y_true, probas_pred의 파라미터를 가지고 있습니다.
- y_true: 실제 클래스값 배열 (배열 크기 = [데이터 건수])
- probas_pred: Positive 칼럼의 예측 확률 배열 (배열 크기 = [데이터 건수])
반환값은 임계값별 정밀도 값을 배열로 반환하고, 재현율도 임곗값별 재현율 값을 배열로 반환합니다.
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)
# 반환된 임계값 배열 로우가 143건이므로 샘플로 10건만 추출하되, 임계값을 15 단위로 추출.
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,)
샘플 추출을 위한 임계값 배열의 index 10개: [ 0 15 30 45 60 75 90 105 120 135]
샘플용 10개의 임계값: [0.1 0.12 0.14 0.15 0.22 0.3 0.48 0.66 0.82 0.93]
샘플 임계값별 정밀도: [0.387 0.415 0.454 0.515 0.571 0.681 0.796 0.868 1. 1. ]
샘플 임계값별 재현율: [1. 0.933 0.9 0.867 0.8 0.783 0.717 0.55 0.383 0.133]
'''
임계값이 높아짐에 따라 정밀도가 높아지면서 재현율이 떨어짐을 알 수 있습니다. 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])
'''
결과1
'''
임계값이 높아지면 재현율이 낮아지고 정밀도가 올라가는 현상이 확실히 눈에 잘 들어옵니다. 임계값이 낮아지면 수치가 반대로 변화합니다. 결과를 보면 임계값이 0.38쯤에서 재현율과 정밀도가 비슷해지는 모습을 보입니다.
정밀도와 재현율의 맹점
정밀도는 TP/(TP + FP)입니다. 정밀도가 100%가 되는 방법은 정말 확실한 경우만 Positive를 부여하는 것입니다. 그렇게 되면, FP=0이 되므로 정밀도가 100가 됩니다.
재현율은 TP/(TP + FN)입니다. 재현율이 100%가 되는 방법은 모든 경우를 Positive로 예측하면 됩니다. 그렇게 되면, FN=0이 되므로 재현율이 100이 됩니다.
재현율과 정밀도는 각 예측 상황에 맞게 어느 것이 더 중요한지 설정할 순 있지만, 한 가지만 보고 판단해서는 안되는 이유입니다.
F1 스코어
F1 Score는 정밀도와 재현율을 결합한 지표로, 정밀도와 재현율이 어느 쪽으로 치우치지 않는 수치를 가질 때 높은 값을 가집니다.
만일 A 예측 모델이 정밀도가 0.9, 재현율이 0.1이라 하고, B 예측 모델이 정밀도가 0.5, 재현율이 0.5라고 할 때, 각각의 F1 Score는 0.18, 0.5로 B 모델이 우수한 F1 Score를 가지게 됩니다.
f1_score()
f1_score() api를 통해 f1 score를 쉽게 구할 수 있습니다.
from sklearn.metrics import f1_score
f1 = f1_score(y_test, pred)
print('F1 스코어: {0:.4f}'.format(f1))
'''
F1 스코어: 0.7500
'''
앞에서 사용했던 예제를 수정해 f1 score도 구해보겠습니다.
# get_clf_eval 수정
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)
# F1 Score 추가
f1 = f1_score(y_test, pred)
print('오차 행렬')
print(confusion)
# f1 score print 추가
print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1:{3:.4f}'.format(accuracy, precision, recall, f1))
pred_proba = lr_clf.predict_proba(X_test)
get_eval_by_threshold(y_test, pred_proba[:, 1].reshape(-1, 1), thresholds)
'''
임계값: 0.4
오차 행렬
[[104 15]
[ 16 44]]
정확도: 0.8268, 정밀도: 0.7458, 재현율: 0.7333, F1:0.7395
임계값: 0.45
오차 행렬
[[104 15]
[ 17 43]]
정확도: 0.8212, 정밀도: 0.7414, 재현율: 0.7167, F1:0.7288
임계값: 0.5
오차 행렬
[[109 10]
[ 18 42]]
정확도: 0.8436, 정밀도: 0.8077, 재현율: 0.7000, F1:0.7500
임계값: 0.55
오차 행렬
[[111 8]
[ 21 39]]
정확도: 0.8380, 정밀도: 0.8298, 재현율: 0.6500, F1:0.7290
임계값: 0.6
오차 행렬
[[111 8]
[ 23 37]]
정확도: 0.8268, 정밀도: 0.8222, 재현율: 0.6167, F1:0.7048
'''
ROC 곡선과 AUC
ROC 곡선과 이에 기반한 AUC 스코어는 이진 분류의 예측 성능 측정에서 중요한 지표입니다. ROC 곡선(Receiver Operation Charateristic Curve)은 우리말로 수신자 판단 곡선으로 불립니다.
ROC 곡선은 FPR(False Positive Rate)을 0부터 1까지 변경하면서 TPR(True Positive Rate)의 변화값을 구합니다. FPR을 X축으로, TPR을 Y축으로 잡으면 곡선 형태로 두 변수의 관계가 나타납니다.
TPR은 재현율과 같으며, TPR = TP/(FN + TP)입니다. 재현율은 민감도로도 불립니다. TPR과 대응되는 지표로 TNR(True Negative Rate)이라 불리는 특이성(Specificity)이 있습니다. TNR = TN/(FP + TN)인데, FPR은 FP/(FP + TN)이므로 1 - TNR 또는 1 - 특이성으로 표현됩니다.
TPR(민감도)는 실제값 Positive가 정확히 Positive로 예측돼야 하는 수준을 나타냅니다.
TNR(특이성)은 실제값 Negative가 정확히 Negative로 예측돼야 하는 수준을 나타냅니다.
FPR을 0부터 1까지 변경하는 방법은 분류 결정 임곗값(Threshold)을 변경하는 것입니다. Classifier가 threshold를 기준으로 positive를 결정하므로, threshold를 1로 설정하면, 모든 경우를 Negative로 판단하게되고 FPR은 0이 됩니다. 반대로, threshold를 0으로 설정하면 모든 경우를 positive로 판단해서 FPR이 1이 됩니다. 따라서 threshold를 1에서 0까지 변화시키며 ROC 곡선을 구하게 됩니다.
roc_curve()
roc_curve()의 주요 입력 파라미터는 아래와 같습니다.
- y_true: 실제 클래스 값 array ( array shape = [데이터 건수] )
- y_score: predict_proba()의 반환값 array에서 Positive 칼럼의 예측 확률이 보통 사용됨. array, shape = [n_samples]
반환값은 아래와 같습니다.
- fpr: fpr 값을 array로 반환
- tpr: tpr 값을 array로 반환
- thresholds: threshold 값 array
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으로 추출.
# thresholds[0]은 max(예측확률)+1로 임의 설정됨. 이를 제외하기 위해 np.arange는 1부터 시작
thr_index = np.arange(1, 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개: [ 1 6 11 16 21 26 31 36 41 46 51]
샘플용 10개의 임곗값: [0.96 0.69 0.63 0.45 0.22 0.18 0.15 0.14 0.12 0.11 0.1 ]
샘플 임곗값별 FPR: [0. 0.025 0.042 0.126 0.311 0.345 0.437 0.571 0.655 0.714 0.798]
샘플 임곗값별 TPR: [0.017 0.533 0.6 0.717 0.817 0.85 0.883 0.9 0.933 0.967 0.983]
'''
임곗값이 1에서 0으로 변함에 따라, FPR이 0에서 1로 커지고 TPR은 더욱 가파르게 커짐을 알 수 있습니다. ROC곡선을 그래프로 시각화해보겠습니다.
def roc_curve_plot(y_test, pred_proba_c1):
# 임곗값에 따른 FPR, TPR 값을 반환받음.
fprs, tprs, thresholds = roc_curve(y_test, pred_proba_c1)
# ROC 곡선을 그래프 곡선으로 그림
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()
roc_curve_plot(y_test, pred_proba[:, 1])
'''결과1'''
ROC 곡선 자체는 FPR과 TPR의 변화값을 보는데 이용하며 분류의 성능 지표로 사용되는 것은 ROC 곡선 면적에 기반한 AUC(Area Under Curve) 값으로 결정합니다. AUC는 ROC 곡선 밑의 면적을 구한 것으로 1에 가까울 수록 좋은 수치입니다. AUC 수치가 커지는 관건은 FPR이 작은 상태에서 얼마나 큰 TPR을 얻을 수 있느냐입니다. 가운데 직선의 AUC는 랜덤 수준의(동전 던지기 수준) 이진 분류 AUC 값으로 0.5입니다. 따라서 보통 분류는 0.5 이상의 AUC 값을 가집니다.
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score
from sklearn.metrics import recall_score, f1_score, roc_auc_score
import numpy as np
print(confusion_matrix(y_test, pred))
print('정확도:', np.round(accuracy_score(y_test, pred), 4))
print('정밀도:', np.round(precision_score(y_test, pred), 4))
print('재현율:', np.round(recall_score(y_test, pred), 4))
print('ROC AUC:', np.round(roc_auc_score(y_test, pred), 4))
'''
[[109 10]
[ 18 42]]
정확도: 0.8436
정밀도: 0.8077
재현율: 0.7
ROC AUC: 0.808
'''
ROC AUC값은 0.8987로 측정됐습니다. 마지막으로 get_clif_eval()함수에 roc_auc_score()를 이용해 ROC AUC값을 측정하는 로직을 추가합니다. ROC AUC는 예측 확률값을 기반으로 계산되므로 이를 get_clif_eval()함수의 인자로 추가합니다.
def get_clf_eval(y_test, pred, pred_proba=None):
confusion = confusion_matrix(y_test, pred)
accuracy = accuracy_score(y_test, pred)
precision = precision_score(y_test, pred)
recall = recall_score(y_test, pred)
f1 = f1_score(y_test, pred)
# ROC-AUC 추가
roc_auc = roc_auc_score(y_test, pred_proba)
print('오차 행렬')
print(confusion)
# ROC-AUC print 추가
print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1:{3:.4f}, \
AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
정리
분류에 사용되는 성능 평가 지표로 정확도, 오차 행렬, 정밀도, 재현율, F1 스코어 , ROC-AUC를 살펴봤습니다. 특히, 이진 분류의 레이블 값이 불균형하게 분포됐을 경우(0이 많고, 1이 적거나 그 반대의 경우) 단순히 예측값과 실제값이 일치하는 지표인 정확도만으로는 머신러닝 모델의 예측 성능을 평가할 수 없습니다.
오차 행렬은 Negative와 Positive값을 가지는 실제 클래스 값과 예측 클래스 값이 True와 False에 따라 TN, FP, FN, TP로 나눠지는 행렬을 기반으로 예측 성능을 평가합니다. 정확도, 정밀도, 재현율 수치는 TN, FP, FN, TP값을 결합해 만들 수 있으며, 이를 통해 분류 모델 예측 성능의 오류가 어떤 모습으로 발생하는지 알 수 있습니다.
정밀도(Precision)와 재현율(Recall)은 Positive 데이터 세트의 예측 성능에 초점을 맞춘 평가 지표입니다. 특히, 재현율은 Positive인 데이터를 Negative로 잘못 판단하게 되면 큰 영향을 줄 수 있는 경우에 사용합니다.(암세포 발견)
분류하려는 업무의 특성에 따라 정밀도와 재현율이 강조되야할 경우 Threshold를 조정해 정밀도 또는 재현율의 수치를 높이는 방법에 대해서 배웠습니다.
F1 스코어는 정밀도와 재현율을 결합한 평가 지표이며, 정밀도와 재현율 어느 한쪽으로 치우치지 않을 때 높은 값을 가지게 됩니다.
ROC-AUC는 일반적으로 이진 분류 성능 평가를 위해 가장 많이 사용되는 지표이며, AUC는 ROC 곡선 밑 면적을 구한 것으로 1에 가까울 수록 좋습니다.
출처: 파이썬 머신러닝 완벽 가이드 (권철민)
'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글
분류 (0) | 2021.11.06 |
---|---|
피마 인디언 당뇨병 예측 (0) | 2021.11.05 |
타이타닉 생존자 예측 (0) | 2021.10.20 |
사이킷런 scikit-learn (0) | 2021.10.15 |
Pandas (0) | 2021.10.10 |