본문 바로가기
  • 조금 느려도, 꾸준히
Artificial Intelligence/Machine Learning

핸즈온 머신러닝(4) - 분류 1

by chan 2020. 1. 21.
반응형

# MNIST

 이번 포스트에서는 너무나도 유명한 MNIST 손글씨 데이터셋을 사용하여 분류 시스템에 대해 다뤄보려고 한다.

사이킷런에서 제공하는 여러 헬퍼 함수를 사용해 잘 알려진 데이터셋을 내려받을 수 있다.

다음은 MNIST 데이터셋을 내려받는 코드이다.

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)

 

데이터셋에서 이미지 하나를 확인해보기 위해 샘플의 특성 벡터를 추출하여 28*28 배열로 크기를 바꾸고 matplotlib의 imshow() 함수를 사용해 그릴 수 있다.

%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt

some_digit = X[36000]
some_digit_image = some_digit.reshape(28, 28)
plt.imshow(some_digit_image, cmap = matplotlib.cm.binary,
           interpolation="nearest")
plt.axis("off")

plt.show()

y[36000] # 9

 

MNIST 데이터셋은 이미 훈련세트와 테스트세트로 나누어 놓았으므로 훈련세트를 섞어서 모든 교차 검증 폴드가 비슷해지도록 한다. numpy random 모듈의 permutation 함수를 사용한다.

import numpy as np

shuffle_index = np.random.permutation(60000)
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]

 

# 이진 분류기 훈련

문제를 단순화하여 하나의 숫자만 식별하는 이진 분류기를 만들어 보자. 여기서는 3과 3이 아닌 두가지 클래스를 구분할 수 있도록 한다. 분류 작업을 위해 타깃 벡터를 만들어보면 다음과 같다.

y_train_3 = (y_train == 3)
y_test_3 = (y_test == 3)

이제 3은 True고, 다른 숫자는 모두 False 인 레이블이 만들어졌다. 이제 분류 모델을 하나 선택해서 훈련해 보는 과정으로 사이킷런의 SGDClassifier 클래스를 사용해 확률적 경사 하강법(Stochastic Gradient Descent, SGD) 분류기로 시작해 보도록 하자. 

 이 분류기는 큰 데이터셋을 효율적으로 처리하는 장점을 지니며 한 번에 하나씩 훈련 샘플을 독립적으로 처리하는 특징이 있다(따라서 온라인 학습에 유리하다).

from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(max_iter=5, random_state=42)
sgd_clf.fit(X_train, y_train_3)

 

 

# 성능 측정

1. 교차 검증을 사용한 정확도 측정

 

cross_val_score() 함수로 폴드가 3개인 K-겹 교차 검증을 사용해 SGDClassifier 모델을 평가해 보도록 한다.

from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_3, cv=3, scoring="accuracy")

# array([0.96545173, 0.93655   , 0.92354618])

정확도가 상당히 높은데, 이에는 한가지 함정이 숨어 있다. 만약 알고리즘이 모든 숫자가 3이 아니라고 예측하면 적어도 90%의 정답은 맞추므로 정확도는 90%가 된다. 이는 정확도를 분류기의 성능 측정 지표로 선호하지 않는 이유가 된다.

 

2. 오차 행렬

 

오차 행렬을 만들기 위해서는 먼저 실제 타깃과 비교할 수 있도록 예측값을 만들어야 한다. 이를 위해 cross_val_predict()를 사용할 수 있다.

from sklearn.model_selection import cross_val_predict

y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_3, cv=3)

 cross_val_predict() 함수는 K-겹 교차 검증을 수행하고 각 테스트 폴드에서 얻은 예측값을 반환한다. 즉 훈련 세트의 모든 샘플에 대해 '깨끗한' 예측을 얻게 된다.

 confusion_matrix() 함수를 사용해 오차 행렬을 만들어 보도록 한다. 

from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_3, y_train_pred)

"""
array([[51061,  2808],
       [  681,  5450]], dtype=int64)
"""

위 결과값 행렬의 행, 열의 의미를 살펴보면 다음 그림과 같다

행렬의 행은 실제 클래스를, 행렬의 열은 예측한 클래스를 나타낸다.

진짜 음성(True Nagative)은 3이 아닌 이미지를 정확하게 분류한 것이고, 진짜 양성(True Positive)은 3인 이미지를 정확히 분류한 것이며 거짓 음성(False Nagative)은 3이 아닌 이미지를 3으로, 거짓 양성(False Positive)은 3인 이미지를 3이 아닌 이미지로 잘못 분류한 것이다.

 

양성 예측의 정확도는 분류기의 정밀도(precision)이라 하며, 식은 다음과 같다

정밀도는 재현율이라는 또 다른 지표와 같이 사용하는 것이 일반적인데, 분류기가 정확하게 감지한 양성 샘플의 비율을 재현율(recall)이라고 하며, 민감도(sensitivity) 또는 진짜 양성 비율이라고도 한다.

3. 정밀도와 재현율

 

사이킷런에서는 정밀도와 재현율을 포함하여 분류기의 지표를 계산하는 여러 함수를 제공하고 있다.

from sklearn.metrics import precision_score, recall_score

precision_score(y_train_3, y_train_pred) # 0.6599660934851054
recall_score(y_train_3, y_train_pred) # 0.8889251345620617

정밀도와 재현율을 F1 점수라고 하는 하나의 숫자로 만들면 유용하다. F1점수는 정밀도와 재현율의 조화 평균 이다.

 

일반화된 조화 평균 식은 위와 같다

 

f1_score() 함수를 호출하여 F1점수를 계산할 수 있다.

from sklearn.metrics import f1_score
f1_score(y_train_3, y_train_pred)

# 0.7575231079296684

정밀도와 재현율은 상황에 따라 중요도가 달라질 수 있다. 정밀도와 재현율 둘 다 향상시키기엔 한계가 있으며, 정밀도를 올리면 재현율이 줄고 반대도 마찬가지로 되는데, 이를 정밀도/재현율 트레이드오프 라고 한다.

 

 

 

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

 

SGDClassifier는 결정 함수(decision function)을 사용해 각 샘플의 점수를 계산한다. 이 점수가 임곗값보다 크면 샘플을 양성 클래스에 할당하고 그렇지 않으면 음성 클래스에 할당하게 되는데, 결정 임곗값을 높이면 상대적으로 더 엄격한 기준치로 양성 클래스를 할당하게 되므로 정밀도는 높아지지만(실제 3이라고 판별한 결과값중 3의 비율이 높아짐), 그렇게 됨으로써 놓치는 타깃이 늘어나므로 재현율은 낮아지게 된다. 반대로 임곗값을 내리면 재현율이 높아지고 정밀도가 줄어든다.

 

사이킷런에서 임곗값을 직접 지정할 수는 없지만 예측에 사용한 점수는 확인할 수 있다. 분류기의 predict() 메서드 대신 decision_function() 메서드를 호출하면 각 샘플의 점수를 얻을 수 있다. 이 점수를 기반으로 임곗값을 정해 예측을 만들 수 있다.

y_scores = sgd_clf.decision_function([some_digit])
y_scores # array([315401.54948065])

threshold = 0 # 임곗값을 0으로
y_some_digit_pred = (y_scores > threshold)
# array([ True]) 

threshold = 200000 # 임곗값을 200000 으로
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
# array([False])

위 코드를 통해 임곗값을 높이면 재현율이 줄어든다는 것을 볼 수 있다.

 

적절한 임곗값을 정하기 위해서는 먼저 cross_val_predict() 함수를 사용해 훈련 세트에 있는 모든 샘플의 점수를 구하고 결정 점수를 반환받도록 지정해야 한다.

y_scores = cross_val_predict(sgd_clf, X_train, y_train_3, cv=3,
                             method="decision_function")

이 점수로 precision_recall_curve() 함수를 사용하여 가능한 모든 임곗값에 대한 정밀도와 재현율을 계산할 수 있다.

from sklearn.metrics import precision_recall_curve

precisions, recalls, thresholds = precision_recall_curve(y_train_3, y_scores)

matplotlib을 이용해 임곗값의 함수로 정밀도와 재현율을 그려보면 다음과 같다.

def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="precision", linewidth=2)
    plt.plot(thresholds, recalls[:-1], "g-", label="recall", linewidth=2)
    plt.xlabel("threshold", fontsize=16)
    plt.legend(loc="upper left", fontsize=16)
    plt.ylim([0, 1])

plt.figure(figsize=(8, 4))
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.xlim([-700000, 700000])
plt.show()

작업에 맞는 최선의 정밀도/재현율 트레이드오프를 만드는 임곗값을 선택하기 위해 재현율에 대한 정밀도 곡선을 그리면 다음과 같다.

def plot_precision_vs_recall(precisions, recalls):
    plt.plot(recalls, precisions, "b-", linewidth=2)
    plt.xlabel("recall", fontsize=16)
    plt.ylabel("precision", fontsize=16)
    plt.axis([0, 1, 0, 1])

plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
plt.show()

프로젝트가 목표하는 결과를 달성하기 위해 필요한 임곗값이 100000이라고 가정하여 그에 따른 예측을 만드려면 분류기의 predict() 메서드를 호출하는 대신 다음 코드를 실행하면 된다.

y_train_pred_goal = (y_scores > 100000)

precision_score(y_train_3, y_train_pred_goal) # 0.8335008375209381
recall_score(y_train_3, y_train_pred_goal) # 0.8116131136845539

 

 

 

5. ROC 곡선

 

 수신기 조작 특성(Receiver Operating Characteristic, ROC) 곡선은 거짓양성비율(FPR) 에 대한 진짜 양성 비율(TPR)의 곡선이다. FPR 은 1 - 진짜음성비율(TNR) 과 같으며, TNR을 특이도(specificity)라고 한다. 즉 ROC 곡선은 민감도(재현율)에 대한 1-특이도 그래프이다.

 

ROC 곡선을 그리기 위해 roc_curve() 함수를 사용해 여러 임곗값에서 TPR과 FPR을 계산한다.

from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_train_3, y_scores)

그런 다음 matplotlib을 사용해 TPR에 대한 FPR 곡선을 나타낸다. 

def plot_roc_curve(fpr, tpr, label=None):
    plt.plot(fpr, tpr, linewidth=2, label=label)
    plt.plot([0, 1], [0, 1], 'k--')
    plt.axis([0, 1, 0, 1])
    plt.xlabel('FPR', fontsize=16)
    plt.ylabel('TPR', fontsize=16)

plt.figure(figsize=(8, 6))
plot_roc_curve(fpr, tpr)
plt.show()

진짜 양성 비율에 대한 거짓 양성 비율 그래프(ROC 곡선)

여기서도 나타나는 트레이드오프는 재현율이 높을수록 분류기가 만드는 거짓양성비율도 늘어난다는 것이다.

점선은 완전한 랜덤 분류기의 ROC 곡선을 뜻한다. 이 점선으로부터 최대한 멀리 떨어져 있어야(왼쪽 모서리 위) 좋은 분류기이다.

 

곡선 아래의 면적(AUC)을 측정하면 분류기를 비교할 수 있는데, 완벽한 분류기는 AUC가 1이고, 완전한 랜덤 분류기는 AUC 가 0.5 이다. 사이킷런은 ROC의 AUC를 계산하는 함수를 제공한다.

from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_3, y_scores)

# 0.9672189436016178

 

RandomForestClassifier를 훈련시켜 SGDClassifier의 ROC곡선과 ROC AUC 점수를 비교해보자.

랜덤포레스트모델에는 decision_function() 메서드가 없기 때문에 predict_proba() 메서드를 이용한다(사이킷런 분류기는 일반적으로 이 두 메서드 중 하나 또는 둘 모두를 가지고 있다.)

 

predict_proba() 메서드는 샘플이 행, 클래스가 열이고 샘플이 주어진 클래스에 속할 확률을 담은 배열을 반환한다.

from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(n_estimators=10, random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_3, cv=3,
                                    method="predict_proba")

ROC 곡선을 그리려면 확률이 아니라 점수가 필요하므로 양성 클래스의 확률을 점수로 사용한다.

y_scores_forest = y_probas_forest[:, 1] # 점수는 양상 클래스의 확률입니다
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_3,y_scores_forest)

비교를 위해 SGD와 랜덤 포레스트 모델의 ROC 곡선을 함께 그려보면 다음과 같다.

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "random forest")
plt.legend(loc="lower right", fontsize=16)
plt.show()

SGD 모델과 랜덤포레스트 모델의 ROC 곡선 비교

 

반응형

댓글