ML

[ML] 분류

tenacy 2022. 7. 1. 02:16
💡
이 글은 <핸즈온 머신러닝 2판> 의 ‘CHAPTER3’ 을 공부하며 정리한 내용이 서술되어 있습니다.
📌
이 글에는 위 책의 범위에 대한 모든 내용이 서술되어 있지 않습니다. 단순히 제가 더 자세히 알아보고 싶은 내용만 서술되어 있으며, 이 또한 정확한 정보가 아니니 읽는 데 주의 바랍니다.

 

MNIST

이 데이터셋은 다음과 같은 특징이 있습니다.

 

  • 고등학생과 미국 인구조사국 직원들이 손으로 쓴 70,000개의 작은 숫자 이미지를 모은 데이터셋
  • 각 이미지에 어떤 숫자를 나타내는지에 대한 레이블이 있음
  • 머신러닝 분야의 ‘Hello World’

 

데이터를 다운로드해봅시다. 주피터 노트북에서 MNIST라는 제목의 파일을 만들어 실습을 진행하겠습니다.

 

from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.keys()

 

 

사이킷런에서 읽어 들인 데이터셋들은 일반적으로 비슷한 구조를 지닙니다.

 

  • DESCR
    • 데이터셋을 설명
  • data
    • 샘플이 하나의 행
    • 특성이 하나의 열로 구성된 배열을 가짐
  • target
    • 레이블 배열을 담음

 

X, y = mnist["data"], mnist["target"]
print(X.shape)
print(y.shape)

 

 

70,000개의 이미지가 있고, 784개의 특성이 있습니다. 이미지는 28x28 픽셀이므로 784개의 특성을 가지는 것입니다. 이미지 하나를 확인해보겠습니다.

 

import matplotlib as mpl
import matplotlib.pyplot as plt

some_digit = X[0]
some_digit_image = some_digit.reshape(28, 28)

plt.imshow(some_digit_image, cmap="binary")
plt.axis("off")
plt.show()

 

 

y[0]

 

 

라벨링도 잘 되어 있군요. 근데 레이블이 문자이므로 이를 수치형 특성으로 변환하는 것이 좋겠습니다.

 

import numpy as np

y = y.astype(np.uint8)
y[0]

 

 

데이터를 조사하기 전, 훈련 데이터셋과 테스트 데이터셋을 준비합니다. MNIST 데이터셋은 훈련과 테스트 데이터셋이 정해져 있기 때문에 별도의 함수를 사용하지 않고, 지정된 위치를 기준으로 나누겠습니다.

 

X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

 

이진 분류기 훈련

현재 데이터는 10개의 레이블을 가지므로 총 10개를 분류해야 하는 상황이지만, 일단 문제를 단순화하여 간단하게 5만 식별해보겠습니다. 일단 이를 위해 타깃 벡터를 만듭니다.

 

y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)

 

이제 분류 모델을 하나 선택해서 훈련해봅시다. 사이킷런의 SGDClassifier 클래스를 사용해 확률적 경사 하강법(SGD) 분류기로 시작해봅니다. 이는 매우 큰 데이터셋을 효율적으로 처리합니다.

 

from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)

 

이제 예측을 수행합니다.

 

sgd_clf.predict([some_digit])

 

 

이 경우에는 정확히 맞췄지만, 모델의 성능은 평가해봐야 합니다.

 

성능 측정

직접 구현하는 교차 검증

앞에서와 마찬가지로, 성능 측정을 위해 교차 검증을 사용할 것입니다. 사이킷런이 제공하는 기능보다 교차 검증 과정을 더 많이 제어해야 하는 경우에는 직접 구현해야 합니다.

 

from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, random_state=42, shuffle=True)

for train_index, test_index in skfolds.split(X_train, y_train_5):
    clone_clf = clone(sgd_clf)
    X_train_folds = X_train[train_index]
    y_train_folds = y_train_5[train_index]
    X_test_folds = X_train[test_index]
    y_test_folds = y_train_5[test_index]
    
    clone_clf.fit(X_train_folds, y_train_folds)
    y_pred = clone_clf.predict(X_test_folds)
    n_correct = sum(y_pred == y_test_folds)
    print(n_correct / len(y_pred))

 

 

k-겹 교차 검증도 해봅시다.

 

from sklearn.model_selection import cross_val_score

cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")

 

 

모든 교차 검증 폴드에 대해 정확도가 95% 이상입니다. 더 나아가 ‘숫자 5 아님’ 클래스로 분류하는 더미 분류기를 만들어 평가해보겠습니다.

 

from sklearn.base import BaseEstimator

class Never5Classifier(BaseEstimator):
    def fit(self, X, y=None):
        return self
    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)

 

여기서 numpy의 zerosdtype 옵션을 가지는데, dtype=bool 일 경우 0이면 False, 1이면 True를 반환합니다. zeros의 반환 리스트의 모든 성분은 0이므로 Never5Classifier는 모두 ‘숫자 5가 아님’을 나타내는 분류기가 되는 겁니다.

 

never_5_clf = Never5Classifier()
cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")

 

 

정확도가 모두 91% 정도로, 이미지의 10% 정도만 숫자 5이기 때문에 무조건 ‘숫자 5 아님’으로 예측하면 정확히 맞출 확률이 90%입니다.

 

훈련 데이터셋을 사용하여 평가를 진행했으니 우리의 훈련 데이터셋이 잘못되지 않은 이상 정확도가 100%여야 할 것입니다. 하지만, 어느 정도의 오차를 보이고 있습니다. 이러한 불균형 데이터셋을 다룰 때 즉, 어떤 클래스가 다른 것보다 월등히 많은 경우 정확도는 성능 측정 지표로서 적합하지 않을 수 있습니다.

 

오차 행렬

오차 행렬은 이러한 상황에서 분류기의 성능을 평가하는 더 좋은 방법입니다.

기본적인 아이디어는 클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 겁니다. 예를 들어, 분류기가 숫자 5의 이미지를 6으로 잘못 분류한 횟수를 알고 싶다면 오차 행렬의 1행 1열을 보면 됩니다.

 

오차 행렬을 만들려면 실제 타깃과 비교할 수 있도록 먼저 예측값을 만들어야 합니다.

cross_val_predict를 사용하여 예측값을 만듭니다. cross_val_predict는 cross_val_score와 마찬가지로 k-겹 교차 검증을 수행하지만 평가 점수를 반환하지 않고 각 테스트 폴드에서 얻은 예측을 반환합니다. 즉, 모든 샘플에 대해 모델이 훈련하는 동안 보지 못했던 데이터에 대해 예측합니다.

 

from sklearn.model_selection import cross_val_predict

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

 

from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_5, y_train_pred)

 

 

  예측 - 숫자 5 아님 예측 - 숫자 5
실제 - 숫자 5 아님 [TN] 53892 [FP] 687
실제 - 숫자 5 [FN] 1891 [TP] 3530

 

오차 행렬의 행은 실제 클래스를 나타내고, 열은 예측한 클래스를 나타냅니다.

1행 1열은 53892개를 ‘숫자 5 아님’으로 정확하게 분류한 걸 나타냅니다(진짜 음성, TN). 1행 2열은 687개를 ‘숫자 5’로 잘못 분류한 걸 나타냅니다(거짓 양성, FP). 이와 마찬가지로, 2행 1열은 1891개를 ‘숫자 5 아님’으로 잘못 분류한 걸 나타내고(거짓 음성, FN), 2행 2열은 3530개를 ‘숫자 5’로 정확하게 분류한 걸 나타냅니다(진짜 양성, TP). 1행은 음성 클래스에 대한 것이고, 2행은 양성 클래스에 대한 것입니다.

 

완벽한 분류기라면 진짜 양성과 진짜 음성만 가지고 있을 겁니다.

따라서, 오차 행렬의 주대각선만 0이 아닌 값이 되겠죠. 하지만 우리의 분류기는 완벽하지 않기 때문에 주대각선이 아닌 성분도 0이 아닌 값이 되는 겁니다.

 

y_train_perfect_predictions = y_train_5
confusion_matrix(y_train_5, y_train_perfect_predictions)

 

 

오차 행렬이 많은 정보를 제공해주지만 더 요약된 지표가 필요할 때도 있습니다. 이 지표들은 다음과 같습니다.

 

  • 정밀도(precision)
    • 양성 예측의 정확도입니다.
    • 확실한 양성 샘플 하나만 예측하면 간단히 완벽한 정밀도를 얻을 수 있지만, 이는 분류기가 다른 모든 양성 샘플을 무시하기 때문에 그리 유용하지 않을 수 있습니다.
    • 정밀도는 재현율이라는 또 다른 지표와 같이 사용하는 것이 일반적입니다.


  • 재현율(recall)
    • 민감도(sensitivity) 또는 진짜 양성 비율(TPR)이라고도 합니다.

 

정밀도와 재현율

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

 

from sklearn.metrics import precision_score, recall_score

print(precision_score(y_train_5, y_train_pred))
print(recall_score(y_train_5, y_train_pred))

 

 

이렇게 보니, 앞서 오차 행렬을 사용하지 않고 평가했을 때처럼 우리의 분류기가 좋아보이지는 않네요. 숫자 5로 판별된 이미지 중 83.7%만 정확하며, 전체 숫자 5에서 65.1%만 감지했습니다.

 

F1 점수(F1 score)는 정밀도와 재현율의 조화 평균입니다.

앞에서와 같은 이유로 정밀도는 일반적으로 재현율과 사용하기 때문에 정밀도와 재현율을 F1 점수라고 하는 하나의 숫자로 만들면 편리할 때가 많습니다.

 

 

f1_score를 사용하여 F1 점수를 계산합니다.

 

from sklearn.metrics import f1_score

f1_score(y_train_5, y_train_pred)

 

 

f1 점수를 사용하면 편리한 것이 사실이지만, 이를 사용하는 것이 항상 바람직한 것은 아닙니다. 상황에 따라 정밀도가 중요할 수도 있고 재현율이 중요할 수도 있기 때문입니다. 다음 두 가지 예로 살펴봅시다.

 

  1. 정밀도가 중요한 경우

어린아이에게 안전한 동영상을 걸러내는 분류기를 훈련시킨다고 가정해봅시다. 재현율은 높으나 나쁜 동영상이 몇 개 노출되는 것보다 좋은 동영상이 많이 제외되더라도(낮은 재현율) 안전한 것들만 노출시키는(높은 정밀도) 분류기를 선호할 것입니다. 나쁜 동영상으로 제대로 예측하는(TP) 경우 뿐만 아니라 좋은 동영상으로 잘못 예측하는(FN) 경우 또한 많아지므로 이는 재현율이 낮아짐을 의미합니다.

 

  1. 재현율이 중요한 경우

감시 카메라를 통해 좀도둑을 잡아내는 분류기를 훈련시킨다고 가정해봅시다. 이 때에는 좀도둑이 한 명이라도 침입하면 큰 타격을 줄 수 있습니다. 따라서, 좀도둑인데 일반 시민 혹은 기타 사물로 잘못 예측하는(FN) 경우가 없도록 해야 합니다. 즉, 재현율이 높아야 합니다. 정확도가 낮아 경비원이 잘못된 호출을 종종 받아 수고스럽겠지만, 거의 모든 좀도둑을 잡을 수 있을 겁니다.

 

유감스럽지만 이 둘을 모두 얻을 수는 없습니다. 정밀도를 올리면 재현율이 줄고 그 반대도 마찬가지입니다. 이를 정밀도/재현율 트레이드오프라고 합니다.

 

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

‘숫자 5’에 대해 분류기가 만든 점수 순으로 이미지를 나열하면 선택 결정 임곗값에 따라 양성 예측(혹은 음성 예측)를 판단할 수 있습니다. 임곗값이 높을수록 재현율은 낮아지고 반대로 정밀도는 높아집니다.

 

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

 

다음은 임곗값이 0일 때의 각 샘플의 점수를 나타냅니다. 임곗값이 0이므로 predict 메서드와 같은 결과를 반환합니다.

 

y_scores = sgd_clf.decision_function([some_digit])
print(y_scores)

threshold = 0
y_some_digit_pred = (y_scores > threshold)
print(y_some_digit_pred)

 

 

임계값을 높여봅시다.

 

threshold = 8000
y_some_digit_pred = (y_scores > threshold)
print(y_some_digit_pred)

 

 

임곗값을 높이니까 재현율이 줄어듭니다. 즉, 이미지가 실제로 숫자 5이고, 임곗값이 0일 때는 분류기가 이를 감지했지만, 임곗값을 8,000으로 높이면 이를 놓치게 됩니다.

 

적절한 임곗값을 정하기 위해 다음의 절차를 따릅니다.

 

  1. cross_val_predict 함수를 사용해 훈련 세트에 있는 모든 샘플의 점수를 구합니다.
    1. 예측 결과가 아닌 결정 점수를 반환받도록 지정합니다.method=”decision_function”
  1. 이 점수로 precision_recall_curve 함수를 사용하여 가능한 모든 임곗값에 대해 정밀도와 재현율을 계산합니다.
  1. matplotlib을 이용해 임곗값의 함수로 정밀도와 재현율을 그리고, 임곗값을 결정합니다.

 

from sklearn.metrics import precision_recall_curve

def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="정밀도")
    plt.plot(thresholds, recalls[:-1], "g-", label="재현율")

y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.show()

 

 

어느 구간에서는 정밀도 곡선이 재현율 곡선보다 더 울퉁불퉁합니다. 재현율은 임곗값이 올라가면 줄어들 수밖에 없지만 정밀도는 임곗값을 올리더라도 가끔 줄어들 수 있습니다.

 

재현율에 대한 정밀도 곡선을 그리는 것도 정밀도/재현율 트레이드오프를 선택하는 좋은 방법입니다.

 

plt.plot(recalls[:-1], precisions[:-1], "b", label="재현율에 대한 정밀도")

 

 

재현율이 80% 근처에서 정밀도 급격하게 줄어듭니다. 일반적으로 이 하강점 직전을 정밀도/재현율 트레이드오프로 선택하는 것이 좋습니다. 하지만 앞서 말했듯이, 이런 선택은 프로젝트에 따라 달라집니다.

 

정밀도 90%를 달성하는 것이 목표라면 다음과 같이 임곗값을 구할 수 있습니다.

 

threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)]
print(threshold_90_precision)

 

 

훈련 세트에 대한 예측을 만들고, 이 예측에 대한 정밀도와 재현율을 확인합니다.

 

y_train_pred_90 = (y_scores >= threshold_90_precision)
print(precision_score(y_train_5, y_train_pred_90))
print(recall_score(y_train_5, y_train_pred_90))

 

 

이렇게 정밀도 90%의 분류기를 만들었습니다. 하지만, 재현율이 너무 낮다면 정밀도가 높은 분류기는 전혀 유용하지 않다는 것에 유의해야 합니다.

 

ROC 곡선

ROC(Receiver operating characteristic) 곡선은 거짓 양성 비율(FPR, 양성으로 잘못 분류된 음성 샘플의 비율)에 대한 진짜 양성 비율(TPR, 재현율의 다른 이름)의 곡선입니다.

 

ROC 곡선은 다음과 같이 그립니다.

 

  1. roc_curve 함수를 사용해 여러 임곗값에서 TPR과 FPR을 계산합니다.
  1. matplotlib을 사용해 TPR에 대한 FPR 곡선을 나타냅니다.

 

from sklearn.metrics import roc_curve

def plot_roc_curve(fpr, tpr, label=None):
    plt.plot(fpr, tpr, linewidth=2, label=label)
    plt.plot([0, 1], [0, 1], 'k--')

fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
plot_roc_curve(fpr, tpr)
plt.show()

 

 

여기에서도 트레이드오프가 있습니다. 재현율(TPR)이 높을수록 분류기가 만드는 거짓 양성(FPR)이 늘어납니다. 점선은 완전한 랜덤 분류기의 ROC 곡선을 뜻하며, 이 점선에서 멀리 떨어질수록 좋은 분류기를 뜻합니다. 즉, ROC 곡선 아래의 면적(AUC)이 1이면 완벽한 분류기이고, 0.5이면 완전한 랜덤 분류기를 뜻합니다.

 

사이킷런에서는 ROC의 AUC를 계산하는 함수를 제공합니다.

 

from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_5, y_scores)

 

 

정밀도/재현율(PR) 곡선과 ROC 곡선에 대해 알아봤습니다. 일반적으로 양성 클래스가 드물거나 거짓 음성(FN)보다 거짓 양성(FP)이 더 중요할 때 PR 곡선을 사용하고, 그렇지 않으면 ROC 곡선을 사용합니다.

 

앞의 분류기는 AUC 점수가 높으므로 좋은 분류기라고 생각할 수 있지만, 이는 음성(5 아님)에 비해 양성(5)이 크게 적기 때문에 나타나는 현상입니다. 즉, 양성 클래스가 드물기 때문에 나타나는 현상입니다. PR 곡선은 이와 다르게, 분류기의 성능 개선 여지가 얼마나 되는지 잘 보여줍니다.

 


ROC 곡선과 PR 곡선의 사용 용도를 그 수식과 관련 지어 생각해봅시다.


 

다중 분류

알고리즘에 따라 다중 분류 가능 여부가 결정됩니다. 즉, 일부 알고리즘은 이진 분류만 가능한 것도 있는데, 이런 이진 분류기를 여러 개 사용해 다중 클래스를 분류하는 기법도 많습니다.

 

예를 들어, 앞에서는 ‘숫자 5’만을 분류하는 이진 분류기를 만들어봤습니다. 각 숫자에 대해 이런 이진 분류기를 만들고, 각 분류기의 결정 점수 중 가장 높은 것을 클래스로 선택하는 전략을 OvR(one-versus-the-rest) 전략이라고 합니다.

 

또한 0과 1 구별, 0과 2 구별, 1과 2 구별 등과 같이 각 숫자의 조합마다 이진 분류기를 훈련시키는 전략을 OvO(one-versus-one) 전략이라고 합니다. 이 전략의 주요 장점은 각 분류기의 훈련에 전체 훈련 세트 중 구별할 두 클래스에 해당하는 샘플만 필요하다는 것입니다.

 

큰 훈련 세트에서 몇 개의 분류기를 훈련시킬 때는 OvR을 선호하고, 서포트 벡터 머신(SVM) 같이 작은 훈련 세트에서 많은 분류기를 훈련시키는 쪽이 빠를 때는 OvO를 선호합니다.

 

다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면 사이킷런이 알고리즘에 따라 자동으로 OvR 또는 OvO를 실행합니다. SVC 클래스를 사용해 서포트 벡터 머신 분류기를 테스트해봅시다.

 

from sklearn.svm import SVC

svm_clf = SVC()
svm_clf.fit(X_train, y_train)
svm_clf.predict([some_digit])

 

 

내부에서는 사이킷런이 OvO 전략을 사용해 45개의 이진 분류기를 훈련시키고 각각의 결정 점수를 얻어 점수가 가장 높은 클래스를 선택합니다.

 

진짜인지 decision_function 메서드를 호출하여 샘플 당 10개의 점수를 확인해봅시다.

 

some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores

 

 

가장 높은 점수가 클래스 5에 해당하는 값입니다.

 

사이킷런에서 OvO나 OvR을 사용하도록 강제하려면 OneVsOneClassifier나 OneVsRestClassifier를 사용합니다. 다음과 같이 이진 분류기 인스턴스를 만들어 객체를 생성할 때 전달하면 됩니다. 예를 들어 다음 코드는 SVC 기반으로 OvR 전략을 사용하는 다중 분류기를 만듭니다.

 

from sklearn.multiclass import OneVsRestClassifier

ovr_clf = OneVsRestClassifier(SVC())
ovr_clf.fit(X_train, y_train)
print(ovr_clf.predict([some_digit]))
print(len(ovr_clf.estimators_))

 

 

SGDClassifier도 훈련시켜 봅시다.

 

sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
sgd_clf.decision_function([some_digit])

 

 

클래스 3의 점수가 1823.7로 가장 높습니다. 분류기를 평가해봅시다.

 

cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")

 

 

스케일을 조정하여 성능을 개선합니다.

 

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")

 

 

에러 분석

괜찮은 모델 하나를 가지고 이 모델의 성능을 향상시키기 위해 에러를 분석해보겠습니다.

 

에러를 분석하기 위해 오차 행렬을 이용합니다. cross_val_predict 함수를 사용해 예측을 만들고 이전처럼 confusion_matrix 함수를 호출합니다.

 

y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
conf_mx = confusion_matrix(y_train, y_train_pred)
conf_mx

 

 

오차 행렬을 matplotlib의 matshow 함수를 사용해 시각화합니다.

 

plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()

 

 

주대각선 부분만 하얗습니다. 이는 대부분의 이미지가 올바르게 분류되었음을 나타냅니다. 하지만, 이 중에서도 숫자 5는 약간 어두운데요. 이는 데이터셋에 숫자 5의 이미지가 적거나 분류기가 숫자 5를 다른 숫자만큼 잘 분류하지 못한다는 뜻입니다.

 

그래프의 에러 부분에 초점을 맞춰 에러 비율을 비교해봅시다. 참고로 MNIST는 클래스별 이미지 개수가 동일하지 않기 때문에 에러의 절대 개수로 오차 행렬을 분석하는 것은 적절하지 않습니다.

 

row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums
np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()

 

 

클래스 8의 열이 밝은 건 많은 이미지가 8로 잘못 분류되었음(낮은 재현율)을 뜻하고, 클래스 8의 행이 어두운 건 실제 8이 적절히 8로 분류되었음(높은 정밀도)을 뜻합니다. 또한, 3행 5열과 5행 3열이 밝은 것을 보아 3과 5가 많이 혼동되고 있다는 것도 알 수 있습니다.

 

cl_a, cl_b = 3, 5
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]

plt.figure(figsize=(8,8))
plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)
plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)
plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)
plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)
plt.show()

 

 

왼쪽 블록 두 개는 3으로 분류된 이미지이고, 오른쪽 블록 두 개는 5로 분류된 이미지입니다. 대부분의 잘못 분류된 이미지는 에러의 정도가 확실함을 알 수 있습니다. 이는 선형 모델인 SGDClassifier를 사용했기 때문에 나타나는 현상입니다. 선형 분류기는 클래스마다 픽셀에 가중치를 할당하고 새로운 이미지에 대해 단순히 픽셀 강도의 가중치 합을 클래스의 점수로 계산합니다. 따라서, 3과 5의 픽셀의 차이가 미미하므로 모델이 쉽게 혼동하게 됩니다.

 

앞서 말했듯이, 3과 5는 픽셀의 차이가 별로 없습니다. 그리고 두 숫자의 생김새를 보아 해당 분류기는 이미지의 위치 및 회전 방향에 매우 민감한 것을 알 수 있습니다. 따라서, 3과 5의 에러를 줄이는 방법으로는 이미지를 중앙에 위치시키고 회전되어 있지 않도록 전처리하는 방법이 있을 겁니다.

 

다중 레이블 분류

지금까지는 각 샘플이 하나의 클래스에만 할당되는 경우였지만, 각 샘플이 여러 클래스에 할당되는 경우도 분명히 존재합니다. 예를 들어, 사진 속에 개만 있는 것이 아니라 개의 주인인 사람도 함께 있을 때 이 둘을 인식해야 하는 경우이죠. 이를 다중 레이블 분류라고 합니다.

 

숫자가 7, 8, 9인지, 홀수인지를 분류하는 다중 레이블 분류를 KNeighborClassifier를 사용하여 수행해보겠습니다. 이 분류기는 다중 레이블 분류를 지원하지만, 다중 레이블 분류를 지원하지 않는 분류기도 존재합니다.

 

from sklearn.neighbors import KNeighborsClassifier

y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]

knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)

 

knn_clf.predict([some_digit])

 

 

숫자 5는 7, 8, 9가 아니고, 홀수이므로 올바르게 분류되었습니다.

 

다중 출력 분류

다중 출력 분류는 다중 레이블 분류에서 한 레이블이 다중 클래스가 될 수 있도록 일반화한 것입니다.

 

이해를 돕기 위해 이미지에서 잡을을 제거하는 시스템을 만들어보겠습니다. 잡음이 많은 숫자 이미지를 입력으로 받아 깨끗한 숫자 이미지를 MNIST 이미지처럼 픽셀의 강도를 담은 배열을 출력합니다. 분류기의 출력이 다중 레이블이고 각 레이블은 값을 여러 개 가지므로 이는 다중 출력 분류 시스템입니다.

 

noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test

 

테스트 세트에서 이미지를 하나 선택합시다. 성능을 평가하는 단계가 아니므로 테스트 데이터셋을 들여다보는 것에 대해 너무 민감하게 반응할 필요는 없습니다.

 

some_index = 0
plt.subplot(121); plot_digit(X_test_mod[some_index])
plt.subplot(122); plot_digit(y_test_mod[some_index])
plt.show()

 

 

분류기를 훈련시켜 이 이미지를 깨끗하게 만듭니다.

 

knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)

 

 

타깃과 매우 비슷한 이미지가 출력되었습니다.