이제 분류 시스템에 대해서 알아보겠습니다.
3.1 MNIST
이 책에서는 고등학생과 미국 인구 조사국 직원들이 손으로 쓴 70,000개의 작은 숫자 이미지인 MNIST 데이터셋을 사용합니다. 이 데이터셋은 학습용으로 자주 사용됩니다.
이 코드를 통해 OpenML.org에서 MNIST 데이터셋을 내려받을 수 있습니다.
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', as_frame=False)
sklearn.datasets에는 대부분 세 종의 함수가 존재합니다.
fetch_* 함수 : 실전 데이터셋 다운로드
load_* 함수 : 사이킷런에 번들로 포함된 소규모 데이터셋 로드
make_* 함수 : 테스트에 유용한 가짜 데이터셋을 생성
이 데이터셋은 일반적을 넘파이 배열이며, (X, y) 튜플로 반환됩니다.
하지만 fetch_openml() 함수는 입력을 판다스 데이터프레임, 레이블을 판다스 시리즈로 반환합니다. 하지만 MNIST 데이터셋은 이미지이므로 데이터프레임이 맞지 않으니 as_frame=False로 지정하여 넘파이 배열로 받습니다.


이렇게 하여 이미지 하나를 확인해볼 수 있습니다. 28 x 28 배열로 크기를 바꾸고 cmap="binary"로 지정하여 0을 흰색, 255를 검은색을 나타냅니다.
데이터를 자세히 조사하기 전에는 항상 테스트 세트를 만들고 따로 떼어놓아야 합니다. 지금의 데이터셋은 이미 훈련 세트(앞쪽 60,000개)와 테스트 세트(뒤쪽 10,000개)로 나뉘어있습니다.
3.2 이진 분류기 훈련
예를 들어 숫자 5를 식별하려고 했을 때 5 or 5 아님 두 개의 클래스를 구분할 수 있는 것이 이진 분류기라고 합니다.
예를 들어 이런 식의 코드를 작성해보겠습니다.
y_train_5 = (y_train == '5') # 5는 True고, 다른 숫자는 모두 False
y_test_5 = (y_test == '5')
이제 사이킷런의 SGDClassifier 클래스를 사용해 확률적 경사 하강법 분류기를 사용해보겠습니다. 이는 매우 큰 데이터셋을 효율적으로 처리할 수 있습니다.
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
이제 이 모델로 숫자를 검증해보면

잘 됩니다.
3.3 성능 측정
이제 성능 측정을 해봅시다.
3.3.1 교차 검증을 사용한 정확도 측정
교차 검증은 모델을 평가하는 좋은 방법입니다.
corss_val_score() 함수로 폴드가 3개인 k-폴드 교차 검증을 사용해 아까 그 모델을 평가해보겠습니다.
k-폴드 교차 검증은 훈련 세트를 k개의 폴드로 나누고, 매번 다른 폴드를 떼어놓고 모델을 k번 훈련합니다.

정확도가 95% 이상이라는 것을 알 수 있습니다!
여기서 모든 이미지를 가장 많이 등장하는 클래스로 구분하는 더미 분류기도 만들어 비교해볼 수 있습니다.
하지만 정확도를 분류기의 성능 측정 지표로 선호하지는 않습니다. 특히 불균형한 데이터셋을 다룰 때는 더욱 그렇습니다. 분류기의 성능을 평가하는 더 좋은 방법은 오차 행렬을 조사하는 것입니다.
3.3.2 오차 행렬
오차 행렬을 만들려면 예측값을 만들어야 합니다. cross_val_predict() 함수를 사용해 만들 수 있습니다.
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
cross_val_predict() 함수는 k-폴드 교차 검증을 수행하지만 평가 점수는 반환하지 않고 각 테스트 폴드에서 얻은 예측을 반환합니다. 즉, 훈련 세트의 모든 샘플에 대해 깨끗한 예측을 얻습니다.
이제 confision_matrix() 함수를 통해 오차 행렬을 만듭니다.

오차 행렬의 행은 실제 클래스를, 열은 예측한 클래스를 나타냅니다.
이 행렬의 첫 번째 행은 '5 아님' 이미지(음성 클래스)에 대한 것, 즉 53,892개를 '5 아님'으로 분류(진짜 음성), 나머지 687개는 '5'라고 잘못 분류한 것입니다.(거짓 양성)
두 번째 행은 '5' 이미지(양성 클래스)에 대한 것으로, 1,891개를 '5 아님'으로 잘못 분류했고(거짓 음성 or 2중 오류) 나머지 3,530개를 정확히 '5'라고 분류했습니다.(진짜 양성)
더 요약된 지표를 얻고 싶다면, 양성 예측의 정확도를 알면 됩니다. 이를 분류기의 정밀도라고 합니다.

TP : 진짜 양성의 수, FP : 거짓 양성의 수
하지만 이는 다른 모든 양성 샘플을 무시하기에 좋지 않습니다. 따라서 재현율이라는 것을 씁니다. 재현율은 분류기가 정확하게 감지한 양성 샘플의 비율입니다.

FN : 거짓 음성의 수

3.3.3 정밀도와 재현율
정밀도와 재현율을 F1 점수라고 하는 하나의 숫자로 만들 수 있습니다. F1 점수는 정밀도와 재현율의 조화 평균입니다.
보통의 평균에 비해 조화 평균은 낮은 값에 훨씬 높은 비중을 둡니다. 즉, F1 점수가 높아지려면 재현율과 정밀도가 모두 높아야 합니다.

F1 점수를 계산하려면 f1_score 함수를 호출할 수 있습니다.

하지만 정밀도와 재현율은 반비례 관계입니다. 이를 정밀도/재현율 트레이드오프라고 합니다.
3.3.4 정밀도/재현율 트레이드오프

이 때 적절한 임곗값을 구하는 것이 중요합니다. 적절한 임곗값을 구하기 위해 cross_val_predict() 함수를 사용하여 훈련 세트에 있는 모든 샘플의 점수를 구합니다.
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
method="decision_function")
이 점수로 precision_recall_curve() 함수를 통해 가능한 모든 임곗값에 대해 정밀도와 재현율을 계산할 수 있습니다.
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
이제 그래프를 그려보죠
plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)
plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)
plt.vlines(threshold, 0, 1.0, "k", "dotted", label="threshold")

좋은 정밀도/트레이드오프를 선택하는 다른 방법은 재현율에 대한 정밀도 곡선을 그리는 것입니다.
import matplotlib.patches as patches # 추가 코드 – 구부러진 화살표를 그리기 위해서
plt.figure(figsize=(6, 5)) # 추가 코드
plt.plot(recalls, precisions, linewidth=2, label="Precision/Recall curve")
plt.plot([recalls[idx], recalls[idx]], [0., precisions[idx]], "k:")
plt.plot([0.0, recalls[idx]], [precisions[idx], precisions[idx]], "k:")
plt.plot([recalls[idx]], [precisions[idx]], "ko",
label="Point at threshold 3,000")
plt.gca().add_patch(patches.FancyArrowPatch(
(0.79, 0.60), (0.61, 0.78),
connectionstyle="arc3,rad=.2",
arrowstyle="Simple, tail_width=1.5, head_width=8, head_length=10",
color="#444444"))
plt.text(0.56, 0.62, "Higher\nthreshold", color="#333333")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.axis([0, 1, 0, 1])
plt.grid()
plt.legend(loc="lower left")
save_fig("precision_vs_recall_plot")
plt.show()

재현율 80% 근처에서 정밀도가 급격하게 줄어드므로 이 하강점 직전을 정밀도/재현율 트레이드오프로 선택하는 것이 좋습니다.
3.3.5 ROC 곡선
수신기 조작 특성(ROC) 곡선도 이진 분류에서 널리 사용됩니다.
ROC 곡선은 거짓 양성 비율(FPR)에 대한 진짜 양성 비율(TPR, 재현율의 다른 이름)의 곡선입니다.
여기서 FPR은 양성으로 잘못 분류된 음성 샘플의 비율입니다. 즉, 1에서 음성으로 정확하게 분류한 음성 샘플의 비율인 진짜 음성 비율(TNR, 특이도라고도 함)을 뺀 값입니다.
즉, ROC 곡선은 민감도(재현율)에 대한 1-특이도 그래프입니다.
3.4 다중 분류
이진 분류기는 두 개의 클래스를 분류하는 반면 다중 분류기(다항 분류기)는 둘 이상의 클래스를 구별할 수 있습니다.
이 때 일부 알고리즘은 여러 개의 클래스를 처리할 수 있지만 우리가 배운 SGDclassifier와 같은 알고리즘은 이진 분류만 가능합니다. 하지만 이진 분류기를 여러 개를 사용해 다중 클래스를 분류할 수 있습니다.
특정 숫자를 구분하는 숫자별 이진 분류기 10개를 0부터 9까지 훈련시킨 클래스가 10개인 숫자 이미지 분류 시스템
이러한 전략을 OvR(one-versus-the-rest) 전략 또는 OvA(one-versus-all) 이라고 합니다.
또 다른 전략은 각 숫자의 조합마다 이진 분류기를 훈련시키는 것인데, 이를 OvO(one-versus-one) 전략이라고 합니다. 이는 클래스가 N개라면 분류기는 N(N-1)/2 개가 필요합니다.
일부 알고리즘은 작은 훈련 세트에서 많은 분류기를 훈련시키는 OvO를 선호하지만, 대부분의 이진 분류 알고리즘에서는 OvR을 선호합니다.
다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면 사이킷런이 알고리즘에 따라 자동으로 OvR나 OvO를 실행합니다.
from sklearn.svm import SVC
svm_clf = SVC(random_state=42)
svm_clf.fit(X_train[:2000], y_train[:2000]) # y_train_5가 아니고 y_train을 사용합니다.
이는 0에서 9까지의 원래 타깃 클래스(y_train)를 사용해 SVC를 훈련시킵니다.

잘 되는 것을 확인할 수 있습니다.
decision_function() 메서드를 호출하면 샘플마다 총 10개의 점수를 반환합니다.

가장 높은 점수는 9.3이고 클래스 5입니다.
3.5 오류 분석
가능성이 높은 모델의 성능을 향상시키는 방법 중 하나는 생성된 오류를 분석하는 것입니다.
먼저 오차 행렬을 살펴봅시다. cross_val_predict() 함수를 사용해 예측을 만들고 confusion_matrix() 함수를 호출합니다.
이 때 오차 행렬을 컬러 그래프로 나타내면 분석하기 훨씬 쉬운데 이 때 ConfusionMatrixDisplay.from_predictions() 함수를 사용하면 됩니다.
from sklearn.metrics import ConfusionMatrixDisplay
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
plt.rc('font', size=9) # 추가 코드 - 폰트 크기를 줄입니다
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred)
plt.show()

이를 정규화하는 것이 중요합니다.
plt.rc('font', size=10) # 추가 코드
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred,
normalize="true", values_format=".0%")
plt.show()

이를 통해 '5' 이미지가 82%만이 올바르게 분류되었다는 것을 쉽게 알 수 있습니다.
여기서 오류를 더 눈에 띄게 만들고 싶다면 올바른 예측에 대한 가중치를 0으로 설정하면 됩니다.
오차 행렬을 분석하면 분류기의 성능 향상 방안에 관한 인사이트를 얻을 수 있습니다. 예를 들어 오차 행렬 스타일로 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)]

3과 5의 주요 차이는 위쪽 선과 아래쪽 호를 이어주는 작은 직선의 위치입니다. 숫자 3을 쓸 때 연결 부위가 조금 왼쪽으로 치우치면 분류기가 5로 분류하고 그 반대도 마찬가지입니다.
따라서 오류를 줄이기 위해서 이미지를 중앙에 위치시키고 회전되어있지 않게 하거나 그렇게 번형 이미지로 훈련 집합을 보강하여 모델이 변형에 잘 견디도록 학습할 수 있습니다. 이를 데이터 증식이라고 합니다.
3.6 다중 레이블 분류
분류기가 샘플마다 여러 개의 클래스를 출력해야 할 때도 있습니다. 여러 개의 이진 꼬리표를 출력하는 분류 시스템을 다중 레이블 분류 시스템이라고 합니다.
예를 들면
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= '7')
y_train_odd = (y_train.astype('int8') % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
이 코드의 첫 번째는 숫자가 큰 값인지 나타내고, 두 번째는 홀수인지 나타냅니다. 그 후 다중 타깃 배열을 사용하여 훈련시킵니다.
이제 예측을 만들면 레이블이 두 개 출력됩니다.

올바르게 분류되는 것을 확인할 수 있습니다. 숫자 5는 크지 않고(False) 홀수(True)이기 때문이죠.
또한 가중치가 다르다면, 레이블에 클래스의 지지도(타깃 레이블에 속한 샘플 수)를 가중치로 줄 수 있습니다. 이렇게 하라면 f1_score() 함수를 호출 한다고 하면 average="weighted"로 설정하며 됩니다.
SVC와 같이 다중 레이블 분류를 지원하지 않는 분류기를 사용하는 경우에는 모델을 체인으로 구성하여 레이블당 하나의 모델을 학습시키면 됩니다.
이는 사이킷런에서 ClassifierChain 클래스를 사용하면 됩니다.
from sklearn.multioutput import ClassifierChain
chain_clf = ClassifierChain(SVC(), cv=3, random_state=42)
chain_clf.fit(X_train[:2000], y_multilabel[:2000])
이제 ClassifierChain을 사용해 예측을 만듭니다.

3.7 다중 출력 분류
마지막으로 다중 출력 다중 클래스 분류(또는 간단히 다중 출력 분류)라는 분류 작업이 있습니다. 이는 다중 레이블 분륭레서 한 레이블이 다중 클래스가 될 수 있도록 일반화한 것입니다.
지금까지 분류에 대한 설명을 모두 하였습니다. 다음 장에서는 지금까지 사용한 모든 머신러닝 모델이 실제로 어떻게 작동하는지 알아보겠습니다. 감사합니다!
'머신러닝 스터디' 카테고리의 다른 글
| [핸즈온 머신러닝 3판] 5장 서포트 벡터 머신 (0) | 2024.11.05 |
|---|---|
| [핸즈온 머신러닝 3판] 6장 결정트리 (0) | 2024.10.29 |
| [핸즈온 머신러닝 3판] 4장 모델 훈련 (2) | 2024.10.01 |
| [핸즈온 머신러닝 3판] 2장 머신러닝 프로젝트 처음부터 끝까지 (3) | 2024.09.16 |
| [핸즈온 머신러닝 3판] 1장 한눈에 보는 머신러닝 (3) | 2024.09.10 |