본문 바로가기

ML

[Machine Learning][3] 분류 (2)

4. 다중 분류

이진 분류기는 두 개의 클래스를 구별하지만 다중 분류기는 둘 이상의 클래스를 구별할 수 있다.

 

이진 분류기를 여러 개를 사용해 다중 클래스로 분류하는 기법도 있다.

데이터를 분류할 때 각 분류기의 결정 점수 중에서 가장 높은 것을 클래스로 선택하면서 진행한다.

이것을 OvR 전략 또는 OvA라고 한다.

 

또 다른 전략은 각 데이터의 조합마다 이진 분류기를 훈련시킨다. 예를 들어 0과 1 구별, 0과 2 구별 등으로 훈련시킨다.

이를 OvO 전략이라고 한다.

OvO 전략의 장점은 각 분류기의 훈련에 구별할 두 클래스에 해당하는 샘플만 있으면 된다는 것이다.

서포트 벡터 머신 같은 알고리즘은 훈련 세트 크기에 예민해 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을 사용합니다.

svm_clf.predict([some_digit])
>>> array(['5'], dtype=object)

 

0~9까지 원래 타깃 클래스로 훈련한다. 10개의 클래스가 있기 때문에 사이킷런은 OvO 전략으로 훈련한다.

예측을 보면 5라고 정확히 맞췄다.

클래스 쌍마다 하나씩 45번의 예측을 수행해 가장 많은 쌍에서 승리한 클래스를 선택했다.

 

some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores.round(2)
>>> array([[ 3.79,  0.73,  6.06,  8.3 , -0.29,  9.3 ,  1.75,  2.77,  7.21,
         4.82]])
         
class_id = some_digit_scores.argmax()
class_id
>>> 5

 

decision_function() 메서드를 호출하면 샘플마다 총 10개의 점수를 반환한다.

가장 높은 점수는 9.3이고 이는 클래스 5에 해당한다.

svm_clf.classes_
>>> array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], dtype=object)

svm_clf.classes_[class_id]
>>> '5'

 

분류기가 훈련할 때 classes_ 속성에 타깃 클래스의 리스트를 값으로 정렬하여 저장한다.

from sklearn.multiclass import OneVsRestClassifier

ovr_clf = OneVsRestClassifier(SVC(random_state=42))
ovr_clf.fit(X_train[:2000], y_train[:2000])

 

OvO나 OvR을 강제로 사용하려면 OneVsOneClassifier나 OneVsRestClassifier를 사용한다.

위 코드는 SVC 기반으로 OvR 전략을 사용하는 다중 분류기를 만든다.

 

ovr_clf.predict([some_digit])
>>> array(['5'], dtype='<U1')

len(ovr_clf.estimators_)
>>> 10

 

 예측은 5로 했고 훈련된 분류기 개수는 10이다.

이제 다중 분류 데이터셋에서 SGDClassifier를 훈련하겠다.

sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
>>>array(['3'], dtype='<U1')

 

예측이 틀렸다. SGD 분류기가 각 클래스에 부여한 점수를 보겠다.

 

sgd_clf.decision_function([some_digit]).round()
>>> array([[-31893., -34420.,  -9531.,   1824., -22320.,  -1386., -26189.,
        -16148.,  -4604., -12051.]])

 

점수를 보면 예측 결과에 강한 확신을 가지고 있다. 클래스 5도 -1,386으로 그리 멀리 있지 않다.

cross_val_score() 함수로 모델을 평가해보겠다.

 

cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
>>> array([0.87365, 0.85835, 0.8689 ])

 

모든 테스트 폴드에서 85.8% 이상을 얻었다. 입력 스케일을 조정하면 정확도를 더 높일 수 있다.

 

from sklearn.preprocessing import StandardScaler

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

>>> array([0.8983, 0.891 , 0.9018])

 

5. 오류 분석

모델의 성능을 향상시키는 방법은 생성된 오류의 종류를 분석하는 것이다.

먼저 오차 행렬을 보겠다.

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()

대부분의 이미지가 주대각선에 있는 것으로 봤을 때, 이미가 올바르게 분류되었다.

5행 5열을 보면 다른 숫자보다 어둡게 보이는 것으로 보아 더 많은 오류를 범했거나 데이터에서 다른 숫자에 비해 5가 적기 때문이다.

따라서 오차 행렬을 정규화해줄 필요성이 있다.

plt.rc('font', size=10)  # 추가 코드
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred,
                                        normalize="true", values_format=".0%")
plt.show()

이제 5 이미지의 82%만이 올바르게 분류되었다는 걸 알 수 있다.

오류를 더 눈에 띄게 만들고 싶다면 올바른 예측에 대한 가중치를 0으로 만들어준다.

 

sample_weight = (y_train_pred != y_train)
plt.rc('font', size=10)  # 추가 코드
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred,
                                        sample_weight=sample_weight,
                                        normalize="true", values_format=".0%")
plt.show()

클래스 8의 열이 매우 밝아진 걸로 보아 많은 이미지가 8로 잘못 분류된 것을 알 수 있다.

백분율을 해석할 땐, 7번 행 9번 열의 36%는 아마자 중 36%가 9로 잘못 분류된게 아니고 7 이미지에서 발생한 오류 중 36%가 9로 잘못 분류되었다는 것이다.

 

오차 행렬은 행 단위가 아닌 열 단위로도 정규화 할 수 있다. normalized='pred'로 지정하면 된다.

이제 성능 향상 방안에 대해 생각해보면 8로 잘못 분류되는 것을 줄이도록 개선해야 한다.

숫자의 훈련 데이터를 많이 모아서 실제 8과 구분하도록 분류기를 학습시키는 방법이 있다.

 

6. 다중 레이블 분류

지금까지는 각 샘플이 하나의 클래스에만 할당되었지만 이제 분류기가 샘플마다 여러 개의 클래스를 출력해야 하는 경우를 보겠다.

예를 들어 분류기가 앨리스, 밥, 찰리 세 얼굴을 인식하도록 훈련되었을 때 앨리사스와 찰리 사진을 본다면 [1,0,1]을 출력한다.

이처럼 여러 개의 이진 꼬리표를 출력하는 분류 시스템을 다중 레이블 분류라고 한다.

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)

 

이 코드는 각 숫자 이미지에 두 개의 타깃 레이블이 담긴y_multilabel 배열을 만든다.

첫 번째는 숫자가 큰 값 (7 이상) 인지 나타내고 두 번째는 홀수인지 나타낸다.

KNeighborsClassifier  인스턴스를 만들고 다중 타깃 배열을 사용해 훈련한다.

 

knn_clf.predict([some_digit])
>>> array([[False,  True]])

 

올바르게 분류되었다. 숫자 5는 크지 않고 (False) 홀수 (True)이다.

다중 레이블 분류기를 이제 평가하겠다. 방법은 다양하지만 이번 방법은 각 레이블의 F1 점수를 구하고 평균 점수를 계산한다.

 

y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
f1_score(y_multilabel, y_train_knn_pred, average="macro")
>>> 0.976410265560605

 

이 코드에서는 모든 레이블의 가중치가 같다고 가정했다. 레이블에 클래스의 지지도를 가중치를 주는 방법도 있다.

f1_score() 함수를 출력할 때 average='weighted'로 설정하면 된다.

 

SVC와 같이 기본적으로 다중 레이블 분류를 지원하지 않을 땐 레이블 당 하나의 모델을 학습시킨다.

하지만 이 방법은 레이블 간의 의존성을 포착하기 어렵다.

이 문제를 해결하기 위해 모델을 체인으로 구성한다. 한 모델이 예측을 할 때 입력 특성과 체인 앞에 있는 모델의 모든 예측을 사용한다.

from sklearn.multioutput import ClassifierChain

chain_clf = ClassifierChain(SVC(), cv=3, random_state=42)
chain_clf.fit(X_train[:2000], y_multilabel[:2000])

chain_clf.predict([some_digit])
>>> array([[0., 1.]])

 

훈련에 진짜 레이블을 사용해 체인 내 위치에 따라 모델에 적절한 레이블을 공급한다.

하지만 cv 하이퍼파라미터를 조정하면 훈련 세트의 모든 샘플에 대해 각 모델에서 표본 외 예측을 얻고, 이러한 예측으로 나중에 체인 안 모든 모델을 훈련한다.

 

7. 다중 출력 분류

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

이를 설명하기 위해 이미지에서 잡음을 제거하는 시스템을 만들겠다.

이 시스템은 잡음이 많은 숫자 이미지를 입력으로 받고 깨끗한 숫자 이미지를 MNIST 이미지처럼 픽셀의 강도를 담은 배열로 출력한다.

분류기의 출력이 다중 레이블이고 각 레이블은 값을 여러 개를 가진다.

np.random.seed(42)  # 동일하게 재현되게 하려고 지정합니다
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

# 추가 코드 – 그림 3–12을 생성하고 저장합니다
plt.subplot(121); plot_digit(X_test_mod[0])
plt.subplot(122); plot_digit(y_test_mod[0])
save_fig("noisy_digit_example_plot")
plt.show()

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

 

knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[0]])
plot_digit(clean_digit)
save_fig("cleaned_digit_example_plot")  # 추가 코드 – 그림 3–13을 저장합니다
plt.show()

타깃과 매우 유사하게 나온 것을 볼 수 있다.