JINWOOJUNG

[ 핸즈온 머신러닝 ] 3. 분류...2(MultiClass Classifier) 본문

핸즈온머신러닝

[ 핸즈온 머신러닝 ] 3. 분류...2(MultiClass Classifier)

Jinu_01 2024. 9. 5. 22:55
728x90
반응형

본 포스팅은 Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow 2판을 토대로

공부한 내용을 정리하기 위한 포스팅입니다. 

해당 도서에 나오는 Source Code 및 자료는 GitHub를 참조하여 진행하였습니다.

https://codingalzi.github.io/handson-ml2/

 

핸즈온 머신러닝

머신러닝/딥러닝 기초 지식 제공

codingalzi.github.io

https://jinwoo-jung.tistory.com/98

 

[ 핸즈온 머신러닝 ] 3. 분류...1(Binary Classifier)

본 포스팅은 Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow 2판을 토대로공부한 내용을 정리하기 위한 포스팅입니다. 해당 도서에 나오는 Source Code 및 자료는 GitHub를 참조하여 진행하였습니

jinwoo-jung.com


 

5인지 아닌지 판단하는 이진 분류기와 달리, 다중 분류기(MultiClass Classifier)는 둘 이상의 클래스를 구별한다. 

 

다중 분류기는 여러 방면으로 접근 가능하다. 

  1. 다중 클래스를 직접 처리
    • SGD, RandomForest, Naive Bayes 분류기 등
  2. 이진 분류기를 여러개 사용
    • OvR(one-versus-the-rest)
    • 특정 숫자 하나만 구분하는 숫자별 이진 분류기 10개(0~9)를 훈련시켜, 이미지 분류 시 각 분류기의 결정 점수 중 가장 높은 클래스를 선택
  3. 발생가능한 경우의 수 만큼 이진 분류기를 사용
    • OvO(one-versus-one)
    • 0과 1, 0과 2 등 발생가능한 경우의 수 만큼의 분류기가 필요하기에, N개의 클래스가 있다면, Nx(N-1)/2 개의 분류기가 요구됨
    • 하나의 분류기를 학습시키는 과정에서 N개의 클래스에 대한 Train Set이 아닌, 구분할 2개의 클래스에 대한 Train Set만 요구됨

 

일반적으로, 이진 분류 알고리즘에서는 OvR이 선호된다. 


다중 분류

 

다중 클래스 분류 작업에서 이진 분류 알고리즘을 선택하면, sklearn이 자동적으로 OvR 또는 OvO를 실행한다.

먼저, SVM Classifier를 테스트 해 보자. 

 

from Function import *

# MNIST 데이터셋 로드
mnist = fetch_openml('mnist_784', version=1)

# X, y를 NumPy 배열로 변환
X, y = mnist["data"].to_numpy(), mnist["target"].to_numpy()
y = y.astype(np.uint8)  # String -> uint8

st_Sample = X[0]

# Train, TestSet 분류
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

from sklearn.svm import SVC

st_SVMClassifier = SVC()
st_SVMClassifier.fit(X_train, y_train)
print(st_SVMClassifier.predict[st_Sample])

 

실행시키면 해당 이미지의 레이블인 5가 정확하게 예측됨을 확인할 수 있다. 다중 클래스 분류작업이기에 y_train_5가 아닌 y_train이 들어갔고 이때, 이진 분류 알고리즘이 사용되었기에 sklearn이 OvO 전략을 선택해 45개의 이진분류기를 훈련시켜 결정 점수가 가장 높은 클래스가 선택됨을 확인할 수 있다. 실제로 그런지 확인 해 보자.

 

st_SampleScore = st_SVMClassifier.decision_function([st_Sample])
print(st_SampleScore)
print(np.argmax(st_SampleScore))
print(st_SVMClassifier.classes_)
print(st_SVMClassifier.classes_[np.argmax(st_SampleScore)])

 

decision_function()을 통해 해당 샘플의 점수를 출력 해 보면, 각각의 클래스에 대한 점수가 나옴을 확인할 수 있다. 그리고 가장 높은 점수를 가지는 클래스의 값이 5임을 확인할 수 있다. 

 

그렇다면, OvO 혹은 OvR을 사용하도록 강제성을 부여할 순 없을까?

from sklearn.multiclass import OneVsRestClassifier
st_SVMClassifierOvR = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
st_SVMClassifierOvR.fit(X_train[:1000], y_train[:1000])   # 첫 1000개의 훈련 샘플만을 이용한 일대다 방식 진행

print(st_SVMClassifierOvR.predict([st_Sample]))
print(len(st_SVMClassifierOvR.estimators_))

 

OneVsRestClassifier()를 통해 OvR로 동작하도록 하였다. 예측 결과도 5로 정확하며, 분류기의 개수는 45개가 아닌 10개임을 확인할 수 있다. 

 

# SGD Classifier - 직접 다중 클래스 분류
st_SGDClassifier = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)
st_SGDClassifier.fit(X_train, y_train)

print(st_SGDClassifier.decision_function([st_Sample]))
print(cross_val_score(st_SGDClassifier, X_train, y_train, cv=3, scoring="accuracy"))

 

SGD Classifier의 경우 직접 다중 클래스 분류가 가능하기에, 따로 OvO, OvR을 설정할 필요는 없다. 각 샘플에 대하여 모든 클래스의 점수를 계산하고, 가장 높은 점수를 가지는 클래스를 선택하는데, 해당 분류기의 경우 3으로 예측하고 있음을 확인할 수 있다. 실제로 다른 클래스의 경우 모두 음수로 3이라고 확신하고 있다. 

 

해당 모델을 평가하기 위해, k=3인 교차 검증을 진행한 결과, 모든 폴드 테스트에 대하여 84% 이상을 얻었다. 실제로 3이라고 혼동할 만큼 첫번째 샘플을 5라고 분류하기에는 아직 모델의 성능이 부족하다. 그렇다면 해당 모델의 성능을 올리기 위해 잘못된 예측을 어떻게 분석할 수 있을까?

 

에러 분석

실제로는 데이터를 준비하는 과정에서 가능한 선택 사항을 탐색하고, 다양한 모델을 시도한 뒤, 가장 좋은 몇갤ㄹ 골라 GridSearchCV를 통해 자동적으로 HyperParameter를 튜닝한다. 

 

이번 파트에서는, 가장 성능이 좋은 모델을 찾았다고 가정하고 에러의 종류를 분석하여 모델의 성능을 향상시킬 방법을 찾아보자.

# Error 분석
y_train_pred = cross_val_predict(st_SGDClassifier, X_train_scaled, y_train, cv=3)
st_ConfusionMatrix = confusion_matrix(y_train, y_train_pred)
print(st_ConfusionMatrix)

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

 

 

먼저 앞서 배운 오차 행렬을 살펴보자. 오차 행렬은 이미지로 표현하는 것이 좋으므로, matplot의 matshow() Method를 사용하였다. 오차 행렬의 주대각선상의 값이 매우 큼으로 해당 모델이 이미지를 잘 분류하고 있음을 확인할 수 있다. 하지만 클래스 5의 경우 다른 클래스보다 어두움을 확인할 수 있다.

 

이는 2가지 경우의 수가 존재하는데,

  1. 클래스 5의 데이터가 다른 클래스에 비해 적다
  2. 해당 모델이 클래스 5를 다른 클래스 만큼 잘 분류하지 못한다. 

에러 분석이므로 2번의 경우에 초점을 맞춰보자. 먼저 오차 행렬의 각 값을 대응되는 클래스의 이미지 개수로 나누어 에러 비율을 비교 해 보자. 이후, 주대각선 상의 값만 0으로 채워 각 클래스가 다른 클래스로 잘못 분류한 에러에 대해 확인 해 보자.

 

row_sums = st_ConfusionMatrix.sum(axis=1, keepdims=True)  # 숫자별 총 이미지 개수
norm_conf_mx = st_ConfusionMatrix / row_sums              # 숫자별 오차율 행렬

np.fill_diagonal(norm_conf_mx, 0)  # 대각선을 0으로 채우기
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()

위 오차 행렬은 오차 행렬에서 각 클래스의 이미지 개수를 나눠준 행렬이다. 위 오차 해렬의 경우 클래스 8의 열이 매우 밝기에, 많은 이미지가 8로 잘못 분류됨을 확인할 수 있다. 위 행렬에서 행은 실제 클래스, 열은 예측한 클래스임을 기억하자. 실제로 클래스 8의 경우 적절히 8로 분류 되었지만, 다른 클래스가 8로 잘못 분류되는 경우가 많다는 것을 확인할 수 있다. 또한,  3과 5 클래스의 경우 서로 혼동하고 있음을 확인할 수 있다.

 

위 오차 행렬을 분석하면, 8로 잘못 분류되는 것을 줄이도록 개선할 필요가 있다. 따라서, 분류기에 도움 될 만한 특성을 찾아야 한다. 예를들면, 원의 갯수 라던지..

 

다중 레이블 분류(Multilabel Classification)

각 샘플마다 하나의 클래스만 할당되었다. 하지만 샘플 내에 다중 객체가 있다면, 예를들어 동물을 분류하는 분류기에 강아지와 고양이가 모두 존재하는 샘플이 주어진다면, 해당 분류기는 둘 다 있다고 분류해야 합니다. 이처럼 다중 클래스를 출력하는 분류 시스템을 다중 레이블 분류 시스템이라 한다. 

 

from Function import *
from sklearn.neighbors import KNeighborsClassifier

# MNIST 데이터셋 로드
mnist = fetch_openml('mnist_784', version=1)

# X, y를 NumPy 배열로 변환
X, y = mnist["data"].to_numpy(), mnist["target"].to_numpy()
y = y.astype(np.uint8)  # String -> uint8

st_Sample = X[0]

# Train, TestSet 분류
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))

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)

print(knn_clf.predict([st_Sample]))

 

각 샘플에 대하여 두 개의 Target Label이 담긴 y_multilabel을 생성한다. 해당 샘플의 숫자가 7이상인지, 홀수인지를 나타내는 Label을 가진다. 다중 레이블이기 때문에 KNeighborsClassifier Instance를 생성한 뒤 훈련시킨다. 

 

 

st_Sample의 경우 숫자 5의 샘플이기에 잘 학습되어 결과가 출력됨을 확인할 수 있다. 

 

다중 레이블 분류기를 평가하는 방법은 모델과 프로젝트 목적에 따라 달라진다. 예를들면, 각 레이블의 $F_1$ 점수를 구하고, 레이블에 대한 가중치를 적용한 평균 점수를 계산할 수 있다. 이때, 타깃 레이블에 속한 샘플 수인 지지도(support)에 따라 가중치를 줌으로써 지지도에 따라 가중치가 달라지는 것을 방지해야 한다. 

 

다중 출력 다중 클래스 분류(MultiOutput-MultiClass Classification)

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

 

예를 들면, 잡음을 제거하는 시스템을 생각 해 보자. 잡음이 많은 숫자 이미지를 입력으로 받아서, 잡음을 제거한 깨끗한 숫자 이미지를 픽셀의 강도(Intensity)를 담은 배열로 출력하는 시스템이다. 

 

from Function import *

# MNIST 데이터셋 로드
mnist = fetch_openml('mnist_784', version=1)

# X, y를 NumPy 배열로 변환
X, y = mnist["data"].to_numpy(), mnist["target"].to_numpy()
y = y.astype(np.uint8)  # String -> uint8

st_Sample = X[0]

# Train, TestSet 분류
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))

# MNIST 훈련 세트의 모든 샘플에 잡음 추가
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise

# MNIST 테스트 세트의 모든 샘플에 잡음 추가
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  # 0번 인덱스 

plt.subplot(121); plot_digit(X_test_mod[some_index])  # 잡음 추가된 이미지
plt.subplot(122); plot_digit(y_test_mod[some_index])  # 원본 이미지

save_fig("noisy_digit_example_plot")
plt.show()

 

MNIST 이미지에서 randint()를 사용하여 픽셀 Intensity에 Noise를 추가하자. 해당 데이터의 레이블은 원본 MNIST 이미지로 한다. 

 

수행 결과 다음과 같이 원본 이미지에 대하여 Noise가 추가되었음을 확인할 수 있다. 이제 KNeighborClassifier를 학습시켜 Noise를 제거한 영상을 예측한 결과를 확인 해 보자. 

 

knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train_mod, y_train_mod)

clean_digit = knn_clf.predict([X_test_mod[some_index]])

plot_digit(clean_digit)
save_fig("cleaned_digit_example_plot")

해당 모델이 원본 MNIST 이미지와 같이 적절히 Noise를 제거했음을 확인할 수 있다. 

728x90
반응형