본문 바로가기
Python (Linux)

22. Python - 다중 분류

by #Glacier 2018. 11. 30.
반응형

안녕하세요. 오늘은 다중 분류에 대해 알아보도록 하겠습니다.

이전에 이진 분류기를 통해 5, 5아님을 구분했다면, 이제 5이상을 감지하는 것을 다중분류라 할 수 있는데요.

앞서서 다중 분류기란 무엇인지 알아보겠습니다.


다중 분류기(multiclass classifier) 또는 다항 분류기(multinomial clssifier)는 둘 이상의 클래스 구별하는 데 사용합니다.

랜덤 포레스트 분류기나 나이브 베이즈 분류기 같은 일부 알고리즘은 여러 개의 클래스 직접 처리가 가능하지만,

서포트벡터머신 분류기나 선형 분류기는 이진분류만 가능합니다.


하지만 이진 분류기를 여러 개 사용하여 다중 클래스를 분류하는 기법도 많습니다. 예를 들어 특정 숫자 하나만 구분하는 숫자별 이진 분류기(0~9까지 10개)를 훈련시켜 클래스가 10개인 숫자 이미지 분류 시스템을 만들 수 있습니다. 그 때, 각 분류기의 결정 점수에서 가장 높은 것을 클래스로 선택하면 됩니다. 


이를 일대 다(one-versus-all, one-versus-the-rest, OvA)전략이라고 합니다.


또 다른 전략은 0과 1 구별, 0과 2 구별, 1과 2 구별 등 각 숫자의 조합마다 이진 분류기를 훈련시키는데,

이를 일대일(one-versus-one , OvO)전략이라고 하며, 클래스가 N개라면 분류기는 N*(N-1)/2개가 필요합니다.

MNIST문제에서는 45개의 분류기를 훈련시켜야 한다는 뜻이 되고,

이미지 하나를 분류하려면 45개의 분류기 모두를 통과시켜서 가장 많이 양성으로 분류된 클래스를 선택합니다. 

OvO전략의 주요 장점은 각 분류기의 훈련에 전체 훈련 세트 중 구별할 두 클래스에 해당하는 샘플만 필요하다는 것입니다.


서포트벡터머신과 같은 일부 알고리즘은 훈련 세트의 크기에 민감해서 큰 훈련 세트에서 몇 개의 분류기를 훈련시키는 것 보다 작은 훈련세트에서 많은 분류기를 훈련시키는 쪽이 빠르므로 OvO를 선호합니다. 하지만 대부분의 이진 분류 알고리즘에서는 OvA를 선호한다.


다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면 사이킷런이 자동 감지하여 OvA를 적용합니다. (SVM분류기 일 때는 OvO)

그럼, SGDClassifier를 통해 예측을 해보겠습니다.


분류를 통해 some_digit (9)를 예측하게 했더니 4라고 나왔습니다. 틀렸죠.

내부에서는 사이킷런이 실제로 10개의 이진 분류기를 훈련시키고, 각각의 결정 점수를 얻어 점수가 가장 높은 클래스를 선택합니다.

정말로 그런지 확인해보겠습니다.


some_digit_scores = sgd_clf.decision_function([some_digit])을 통해 결정 점수를 반환받습니다.

이 메서드는 샘플 당 1개의 점수가 아닌, 클래스마다 하나씩 총 10개의 점수를 반환합니다. 이렇게 확인해볼 수 있습니다.


분류기가 훈련될 때 classes_속성에 타깃 클래스의 리스트 값을 정렬하여 저장합니다. 위 예제에서는 classes_배열에 있는 각 클래스의 인덱스가 클래스 값 자체와 같지만 일반적으로 이런 경우는 드뭅니다.


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



OneVsOneClassifier로 훈련시키고 예측하였더니 9라는 숫자로 맞추었습니다.


분류기가 45개인 것도 확인할 수 있습니다.


이번에는, RandomForestClassifier를 훈련시켜봅니다. 랜덤포레스트 분류기는 직접 샘플을 다중 클래스로 분류할 수 있기 때문에 별도로 사이킷런의 OvA나 OvO를 적용할 필요가 없습니다. predict_proba()메서드를 호출하면, 분류기가 각 샘플에 부여한 클래스별 확률을 얻을 수 있습니다.



predict_proba를 이용하여 클래스별 확률을 보면, 분류기가 예측 결과에 엄청난 확신을 가지고 있음을 알 수 있습니다.

9라는 것에 100% 확신했음을 알 수 있습니다.


이제 분류기를 평가하는 방법에 대해 알아볼 건데요. 교차 검증을 통한 정확도 측정을 해봅니다.



교차 검증을 통한 SGDClassifier분류기는 모든 폴드에서 83%이상의 정확도를 가지고 있음을 알 수 있습니다.



RandomForestClassifier는 모든 폴드에서 94%이상의 정확도를 가지고 있음을 알 수 있어 SGDClassifier보다 정확하다고 평가할 수 있겠네요.



SGDClassifier는 입력 스케일을 조정하여 정확도를 90%이상으로 높일 수 있습니다.


이제, 에러 분석에 대해 알아보겠습니다. 에러 분석에 앞서 가능성이 높은 모델을 찾았다고 가정하고, 이 모델의 성능을 향상시킬 방법을 찾는데요. 그 중 한 가지 방법은 만들어진 에러의 종류를 분석하는 것입니다. 이를 위해서 오차행렬을 만듭니다.



y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3) 코드로 예측에 대한 값을 받고,

conf_mx = confusion_matrix(y_train, y_train_pred)로 타깃값과 예측값에 대한 Confusion Matrix를 만듭니다.

오차행렬을 시각화하고, 대부분의 이미지가 올바르게 분류되었음을 나타내는 주대각선에 있으므로 매우 좋아보입니다.

숫자 5는 다른 숫자들보다 조금 더 어두운 것을 볼 수 있고, 이는 데이터셋에 숫자 5의 이미지가 적거나, 분류기가 숫자 5를

다른 숫자만큼 분류하지 못한다는 의미입니다. 이 두 경우 모두 확인이 필요합니다.


이제, 오차행렬의 각 값에 대응되는 클래스의 이미지 개수로 나누어 에러 비율을 비교합니다.

(에러 개수X, 에러 개수로 비교하면 이미지가 많은 클래스가 상대적으로 나쁘게 보입니다.)


다른 항목은 그대로 두되, 주대각선만 0으로 채워서 그래프를 그립니다.


배열에서 가장 큰 값은 흰색으로, 가장 작은 값은 검은색으로 표시됩니다. (MNIST는 클래스 별 이미지 개수가 동일하지 않습니다.)

행은 실제 클래스, 열은 예측한 클래스로 나타납니다.


전체적인 열에서 8, 9 열이 밝은 것으로 보아 많은 숫자들이 8이나 9로 잘못 분류되고 있음을 암시하고, 8, 9의 행도 밝은 것으로 보아 8,9가 다른 숫자들과 자주 혼동된다는 것을 말해줍니다. 반대로 클래스 1행은 매우 어두워 대부분 잘 분류되고 있음을 알 수 있습니다.


성능적인 측면에서, 3을 5로 혼동하거나, 5를 3으로 혼동하는 부분이 많다는 것을 알 수 있습니다. (흰색) 

그리고, 8과 9가 혼동되는 만큼 3,5,8,9에 대한 훈련 데이터를 더 모을 필요가 있습니다.

또, 동심원을 세는 알고리즘 같은 것을 도입하는 것도 좋을 것입니다.(8은 2개, 6은 1개 5는 0개)

혹은 동심원 같은 어떤 패턴이 드러나도록 Scikit-Image, Pillow, OpenCV등을 이용하여 이미지를 전처리해볼 수도 있습니다.


아래의 정의된 함수를 만든 후, plot으로 나타내면, 

개개의 에러를 분석해보면 분류기가 무슨 일을 하고 있고 왜 잘못되었는지 일일이 분석하는 것 보다 쉽게 접근할 수 있습니다.



3과 5로 분류된 두 개의 이미지들을 보면, 분류기가 잘못 분류한 숫자 중 일부는 정말 잘못 쓰여 있어서 그럴 수도 있겠다는 생각이 듭니다. 그러나, 대부분 잘못 분류된 이미지는 확실히 에러인 것 같고, 분류기가 실수한 이유를 이해하기 어렵습니다. (사람의 머리는 환상적인 패턴 인식 시스템을 가지고 있기 때문에, 당연한 것도 당연한 것이 아닐 수도 있음)


이 원인은, 선형 모델인 SGDClassifier를 사용했기 때문입니다. 선형 모델은 클래스마다 픽셀에 가중치를 할당하고, 새로운 이미지에 대해 단순히 픽셀 강도의 가중치 합을 클래스의 점수로 계산합니다. 따라서 3과 5는 몇 픽셀만 다르기 때문에 모델이 쉽게 혼동하게 됩니다. 


다른 말로 하면, 분류기는 이미지의 위치나 회전 방향에 매우 민감하기 때문에 3과 5의 에러를 줄이는 한 가지 방법은 이미지를 중앙에 위치시키고, 회전되어 있지 않도록 전처리하는 것입니다. (또한 이 그림들의 픽셀 자체가 작기 때문에 더 큰 사진도 좋습니다.)


이제, 다중 레이블 분류에 대해 알아보겠습니다. 지금까지는 각 샘플이 하나의 클래스에만 할당되었습니다. 하지만 분류기가 여러 샘플마다 여러 개의 클래스를 출력해야 하는 경우도 있습니다. 만약, 얼굴 인식기라고 한다면, 사진에 있는 여러 사람마다 레이블을 하나씩 할당해야 합니다. 엘리스, 밥, 찰리가 있을때 밥이 없다면 [1,0,1] 처럼 이진 레이블을 출력하는 분류 시스템을 다중 레이블 분류(multilabel classification)이라 합니다.



이 코드는 각 숫자 이미지에 두 개의 타깃 레이블이 담긴 y_multilabel 배열을 만듭니다. 첫 번째는 숫자가 큰 값(7,8,9) 인지 나타내고, 두 번째는 홀수인지 나타냅니다. 그 다음 줄이 KNeighborsClassifier 인스턴스를 만들고, 다중 타깃 배열을 사용하여 훈련시킵니다.

(KNeighborsClassifier는 다중 레이블 분류를 지원하지만 모든 분류기가 그런 것은 아닙니다. 지원하는 분류기는 DecisionTree, RandomForest, OneVsRestClassifier가 있습니다.)



우리의 some_digit은 9이기 때문에 7보다 크므로 True, 홀수이므로 True입니다.


이렇게 만든 다중 레이블 분류기를 평가하는 방법 중 적절한 지표는 프로젝트마다 다릅니다.

예를 들어, 각 레이블의 F1점수를 구하고, (또는 어떤 이진 분류지표를 이용) 간단하게 평균 점수를 계산합니다.


실제로는 아니지만, 이 코드는 모든 레이블의 가중치가 같다고 가정한 것입니다. 특히 앨리스 사진이 밥이나 찰리 사진보다 훨씬 많다면 앨리스 사진에 대한 분류기의 점수에 더 높은 가중치를 둘 것입니다. 간단한 방법은 레이블에 클래스의 지지도(support, 즉 타깃 레이블에 속한 샘플 수)를 가중치로 주는 것입니다. 이렇게 하려면 이전 코드에서 average="weighted"로 설정하면 됩니다.


(사이킷런은 몇 가지 다른 평균 계산 방식과 다중 레이블 분류기 지표를 제공합니다. average="micro" 옵션은 FP, FN, TP 총합을 이용해 F1점수를 계산합니다. 

 accuracy_score, precision_score, classification_report 함수 등이 다중 분류를 지원합니다. 자세한 내용은 공식 문서 참조)


마지막으로 알아볼 분류 작업은 다중 출력 다중 클래스 분류(multioutput-multiclass classification) 또는 간단히 다중 출력 분류(multioutput classification)입니다. 다중 레이블 분류에서 한 레이블이 다중 클래스가 될 수 있도록 일반화한 것입니다. (즉, 값을 두 개 이상 가질 수 있습니다.)


이를 위해 이미지에서 노이즈를 제거하는 시스템을 만들어보겠습니다. 노이즈가 많은 숫자 이미지를 입력으로 받고, 깨끗한 숫자 이미지를 MNIST이미지처럼 픽셀의 강도를 담은 배열로 출력합니다. 분류기의 출력이 다중 레이블(픽셀 당 한 레이블)이고 각 레이블은 여러 개의 값을 가집니다. (0부터 255까지 픽셀 강도) 그러므로 이 예는 다중 출력 분류 시스템입니다.


먼저, MNIST 이미지에서 추출한 훈련 세트와 테스트 세트에 넘파이의 randint() 함수를 사용하여 픽셀 강도에 노이즈를 추가합니다.


완전히 깨끗하지는 않지만, 타깃과 비슷한 형태를 띄고 있음을 알 수 있네요.

이것으로 분류에 대한 학습을 마치고, 다음 장으로 넘어가겠습니다.


 블로그 

출처


이 글의 상당 부분은  [핸즈온 머신러닝, 한빛미디어/오렐리앙 제롱/박해선] 서적을 참고하였습니다.


나머지는 부수적인 함수나 메서드에 대해 부족한 설명을 적어두었습니다.

학습용으로 포스팅 하는 것이기 때문에 복제보다는 머신러닝에 관심이 있다면 구매해보시길 추천합니다.


도움이 되셨다면 로그인 없이 가능한

아래 하트♥공감 버튼을 꾹 눌러주세요! 



반응형