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

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

by chan 2020. 1. 21.
반응형

# 다중 분류

 다중 분류기를 구현하는 기법으로는 랜덤 포레스트 분류기나 나이브 베이즈(naive Bayes)분류기 같은 알고리즘으로 여러 개의 클래스를 직접 처리하거나, 이진 분류기를 여러 개 사용해 다중 클래스를 분류하는 방법이 있다.

 

이진 분류기를 여러 개 사용하여 다중 클래스를 분류할 때에는 각 분류기의 결정 점수 중에서 가장 높은 것을 클래스로 선택하게 되는데, 이를 일대다(one-versus-all, OvA)전략 이라고 한다.

 

혹은 숫자를 분류할 때 0과 1 구별, 0과 2 구별, 1과 2 구별 등과 같이 각 클래스의 조합마다 이진 분류기를 훈련시키는 방법이 있는데, 이를 일대일(one-versus-one, OvO)전략이라고 한다. 이때 클래스가 N 개라면 분류기는 N(N-1)/2 개가 필요하다. OvO전략의 주요 장점은 각 분류기의 훈련에 전체 훈련 세트 중 구별할 두 클래스에 해당하는 샘플만 필요하다는 것이다.

 

다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면 사이킷런이 자동으로 감지해 OvA(SVM일 경우 OvO)를 적용한다.

sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit]) # array([3])

위 코드는 0에서 9까지의 원래 타깃 클래스를 사용해 SGDClassifier를 훈련시키고 예측 하나를 만든다. 내부에서 실제로 사이킷런이 10개의 이진 분류기를 훈련시키고 각각의 결정 점수를 얻어 점수가 가장 높은 클래스를 선택한다.

 

정말 그런지 확인하려면 decision_function() 메서드를 호출하여 클래스마다 결정 점수가 원소로 된 배열을 결과값으로 얻어 볼 수 있다.

 

사이킷런에서 OvO 나 OvA를 사용하도록 강제하려면 OneVsOneClassifier 나 OneVsRestClassifier를 사용한다.  다음 코드는 SGDClassifier 기반으로 OvO 전략을 사용하는 다중 분류기를 만드는 것이다. 

from sklearn.multiclass import OneVsOneClassifier
ovo_clf = OneVsOneClassifier(SGDClassifier(max_iter=5, random_state=42))
ovo_clf.fit(X_train, y_train)
ovo_clf.predict([some_digit])

 

RandomForestClassifier 역시 간단하게 훈련시킬 수 있다.

forest_clf.fit(X_train, y_train)
forest_clf.predict([some_digit])

이 경우 랜덤포레스트 분류기는 직접 샘플을 다중 클래스로 분류할 수 있기 때문에 별도로 사이킷런의 OvA 나 OvO를 적용할 필요가 없다. predict_proba() 메서드를 이용해 SGD모델의 decision_function() 메서드와 비슷한 결과를 볼 수 있다.

 

분류기를 평가할 때는 일반적으로 교차 검증을 사용하므로 cross_val_score() 함수를 사용해 SGDClassifier의 정확도를 평가할 수 있다.

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

# array([0.87737453, 0.88699435, 0.86142921])

 

간단히 입력 스케일을 조정하여 정확도를 90% 이상으로 높여보자.

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

# array([0.91081784, 0.90969548, 0.91158674])

 

 

 

 

 

# 에러 분석

모델의 성능을 향상시키기 위한 한 가지 방법으로 에러의 종류를 분석할 수 있다.

먼저 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

"""
array([[5734,    3,   23,    9,   11,   43,   42,    9,   43,    6],
       [   2, 6478,   41,   29,    6,   42,    5,   11,  117,   11],
       [  58,   37, 5354,   91,   79,   24,   87,   57,  157,   14],
       [  51,   36,  143, 5349,    1,  236,   36,   51,  138,   90],
       [  18,   28,   48,    8, 5380,    8,   46,   30,   73,  203],
       [  74,   42,   36,  182,   75, 4608,  103,   27,  174,  100],
       [  34,   25,   46,    2,   43,   88, 5632,    4,   44,    0],
       [  28,   20,   74,   32,   57,   11,    6, 5769,   16,  252],
       [  51,  155,   71,  148,   13,  152,   47,   24, 5051,  139],
       [  41,   32,   29,   89,  178,   28,    3,  173,   89, 5287]],
      dtype=int64)
"""

오차 행렬을 matplotlib의 matshow() 함수를 사용해 이미지로 표현하면 보기에 편리할 때가 많다.

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

위 이미지의 주대각선이 밝을수록 분류기가 분류를 잘 했다는 것이다. 각 대각 원소마다 밝기가 다른데, 이는 어두운 원소에 해당하는 숫자의 이미지가 적거나 분류기가 그 숫자를 다른 숫자만큼 잘 분류하지 못했다는 뜻이므로 두가지 경우에 대해 모두 확인해봐야 한다.

 

그래프의 에러 부분에 초점을 맞추어 오차 행렬의 각 값을 대응되는 클래스의 이미지 개수로 나누어 에러 비율을 비교한다.

row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums

주대각선만 0으로 채워 그래프를 그려보면 다음과 같다.

np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()

행은 실제 클래스를 나타내고 열은 예측한 클래스를 나타낸다.

위 그래프를 보면, 분류기가 3과 5를 혼동하는것을 보완하고 8과 9를 더 잘 분류할 수 있도록 개선할 필요가 있어 보인다. (3행5열, 5행3열의 밝기가 밝고, 8열과 9열, 8행과 9행이 대체적으로 밝게 나타나므로)

 

이러한 에러들을 분석하고, 데이터의 문제인지 모델의 문제인지 추정하여 상황에 따라 적절한 처리를 해주는 것이 무엇보다 중요하다. 

 

 

 

 

 

 

# 다중 레이블 분류

분류기가 샘플마다 여러 개의 클래스를 출력해야 할 때도 있다. 예를 들어 같은 사진에 여러 사람이 등장한다면 얼굴 인식 분류기는 인식된 사람마다 레이블을 하나씩 할당해야 한다. 여러 개의 이진 레이블을 출력하는 분류 시스템을 다중 레이블 분류 시스템 이라 한다.

 

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)

위 코드는 각 숫자 이미지에 두 개의 타깃 레이블이 담긴 y_multilabel 배열을 만든다. 첫번째는 숫자가 7이상의 값인지 나타내고 두번째는 홀수인지 나타낸다. 위에서는 다중 레이블 분류를 지원하는 KNeighborsClassifier모듈을 이용하여 훈련시켰다.

 

다중 레이블 분류기를 평가하는 방법은 많이 있으며 프로젝트에 따라 적절한 지표는 다르지만, 예제에서는 간단히 각 레이블의 F1점수를 구하고 평균 점수를 계산한다.

y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3, n_jobs=-1)
f1_score(y_multilabel, y_train_knn_pred, average="macro")

# 0.9768224011622285

 * 위 코드를 실행할 때 주의할 점: 매우 오래 걸린다. 하드웨어에 따라서 몇 시간이 걸릴 수 있으므로 주의하도록 한다

 

위 코드는 모든 레이블의 가중치가 같다고 가정한 것이다. (average="macro") 가중치를 두기 위한 가장 간단한 방법은 레이블에 클래스의 지지도(support, 타깃 레이블에 속한 샘플 수)를 가중치로 주는 것이다. 이때 코드를 average="weighted"로 설정한다.

 

# 다중 출력 분류

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

 

예시로 노이즈가 섞인 숫자 이미지를 깨끗한 숫자 이미지로 반환하는 분류기를 만들어 본다.

 

numpy의 randint() 함수를 사용하여 픽셀 강도에 노이즈를 추가한다. 타깃 이미지는 원본 이미지로 한다.

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 = 5500
plt.subplot(121); plot_digit(X_test_mod[some_index])
plt.subplot(122); plot_digit(y_test_mod[some_index])
plt.show()

왼쪽이 노이즈가 섞인 이미지, 오른쪽이 타깃 이미지

K-neariest Neighbor 분류기를 사용해 훈련시켜 이미지를 깨끗하게 만들어 본다.

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

분류 포스트를 통하여 분류 작업에서 좋은 측정 지표를 선택하고, 적절한 정밀도/재현율 트레이드오프를 고르고, 분류기를 비교하는 방법에 대해 알아보았다. 

반응형

댓글