본문 바로가기
Python (Linux)

21. Python - MNIST를 이용한 분류 기초와 성능 평가 지표

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

안녕하세요. 이번에는 3장 분류에 대해 알아보고자 합니다.

2장에서는 주택 가격을 예측하는 회귀 작업을 보면서, 선형 회귀, 결정 트리, 랜덤 포레스트 알고리즘을 보았습니다.


이번 장에서는 분류 시스템을 집중적으로 다룹니다.

이 장에서 다룰 데이터셋은, 유명한 MNIST 인데요. 

바로 고등학생과 미국 인구조사국 직원들이 손으로 쓴 70,000개의 작은 숫자 이미지를 모은 데이터입니다.

각 이미지에는 어떤 숫자를 나타내는지 레이블되어 있습니다.


이 데이터셋은 학습용으로 아주 많이 사용되기 때문에 머신러닝 분야의 'Hello World'라고도 불리곤 한다고 합니다.

새로운 분류 알고리즘이 나올 때마다 MNIST 데이터셋에서 얼마나 잘 작동하는지 본다고 하네요.


(하지만 삼성SDS 테크토닉에서는 MNIST 데이터셋은 이미지 자체가 작기 때문에 잘 작동하지만, 

 노이즈를 조금만 줘도 확 떨어진다고도 했었던 기억이..)



어쨌거나! 시작하면, sklearn.datasets의 fetch_openml을 이용합니다.

mnist = fetch_openml(data_id=554) 이렇게 하면 되는데요!

혹시 여기서 Errno-3 urlopen 오류가 나신다면,

urllib3을 깔아주어야 합니다.


terminal에서 pip install urllib3 하시면 되는데. 이것도 일시적인 오류라며 안깔리는 경우가 있습니다.

이 때는, Terminal에서 vi /etc/resolv.conf 에서 nameserver 똑같이 쓰시고, IP를 써주신 후 저장하시면 됩니다.

ip확인은 ifconfig 치면 되고, vi환경에서 쓰기모드는 i , 나가기는 esc + shift+Z + wq 쓰신후 나가면 됩니다.


mnist의 target은 글자로 인식되어있기 때문에, numpy.int를 통해서 정수형으로 바꿔주는 것이 좋습니다.



이제, 데이터 중 하나를 뽑아서, matplotlib의 plt.imshow()를 이용해 그려봅니다.

9처럼 보이는데요. 확인을 위해서 레이블 데이터에서 똑같은지 확인하면, 9가 나오면서! 맞네요.



이전에, 데이터를 다루기 전 훈련 데이터와 테스트 데이터셋을 나눠두어야 한다고 했는데요.

사실 이 데이터들은 앞 6만 개의 데이터가 훈련 셋, 뒤의 1만개는 테스트용 으로 나누어져 있습니다.


이제, 이진 분류기를 만들어봅니다.


shuffle을 이용하여 데이터들을 섞어서 모든 교차 검증 폴드가 비슷해지도록 만듭니다.

어떤 학습 알고리즘은 훈련 샘플의 순서에 민감해서, 비슷한 샘플이 연이어 나오면 성능이 나빠지므로 데이터셋을 섞으면 이러한 일을 방지할 수 있지만, 시계열 데이터와 같이 시간의 흐름에 따른 추세가 있는 데이터들은 섞으면 오히려 역효과가 나겠죠!

이제, 5감지기를 만들기 위해서 5이면 True 아니면 False를 출력하는 타깃 벡터를 만듭니다.



SGDClassifier의 확률적 경사 하강법(Stochastic Gradient Descent, SGD)를 이용합니다. 

이 분류기는 매우 큰 데이터셋을 효율적으로 처리하는 장점을 지니고 있는데요. 

그 이유는 SGD가 한 번에 하나씩 훈련 샘플을 독립적으로 처리하기 때문입니다.

그래서 SGD가 온라인 학습에 잘 들어맞습니다. 이제 SGD 모델을 만들고, 전체 훈련 세트를 사용해 훈련시킵니다.




오류가 뜨는데요! 이는 SGDClassfier에서 max_iter를 지정해주었는데 tol을 지정해주지 않은 경우 None으로 간주되며,

-infinity로 동등하게 취급되어 영향을 주지 않는다고 경고합니다. 저자는 이에 대해 별 말이 없는 것으로 보아

큰 문제는 아니라고 생각됩니다.


학습시킨 분류기를 some_digit(9)를 통해 판독하게 하면, False로 잘 구분해내는 것을 알 수 있습니다.

이제 이 SGD 분류기의 성능을 평가해볼 차례인데요!


분류기 평가는 회귀 모델보다 훨씬 어렵기 때문에 이 장에서는 많은 지면을 할애할 것이라고 합니다.

사용할 수 있는 성능 지표가 많으니 참고해볼 만 하겠네요.


첫 번째로, 교차 검증 구현입니다.

사이킷런이 제공하는 기능보다 교차 검증 과정을 더 많이 제어해야 할 경우가 있는데,

이 때는 교차 검증 기능을 직접 구현하면 됩니다.


다음 코드는 사이킷런의 cross_val_score()함수와 거의 같은 작업을 수행하고 동일한 결과를 출력합니다.



StratifiedKFold는 클래스별 비율이 유지되도록 폴드를 만들기 위해 계층적 샘플링을 수행합니다.

매 반복에서 분류기 객체를 복제하여 훈련 폴드로 훈련시키고, 테스트 폴드로 예측을 만듭니다.

그런 다음, 올바른 예측의 수를 세어 정확한 예측의 비율을 출력합니다.


이번에는 cross_val_score() 함수로 폴드가 3개인 K-겹 교차 검증을 사용하여 SGDClassifier모델을 평가해보겠습니다.

K-겹 교차 검증은 훈련 세트를 K개(여기서는 3개)의 폴드로 나누고, 각 폴드에 대해 예측을 만들고 평가해서,

나머지 폴드로 훈련시킨 모델을 사용한다는 것을 기억하세요.


즉, 우리가 데이터를 검증하기 위해 2가지의 방법을 사용하는데,

데이터셋을 이용하여 Train Set, Test Set으로 나누고 Train Set으로 학습, Test Set으로 학습의 정확도를 측정하는 것을

크로스 밸리데이션(Cross-validation)이라고 합니다.


두 번째는 사용하고자 하는 알고리즘의 매개변수를 조사하여 가장 성능이 좋을 때의 매개변수 값을 찾는 방법을

그리드 서치(Grid Search)라고 합니다.



이렇게 사이킷런의 cross_val_score()함수를 통해 SGD Classifier모델을 평가하여도 똑같은 Accuracy가 나오네요.

즉, 모든 교차 검증 폴드에 대해 정확도가 95% 이상임을 알 수 있습니다.

이제, 모든 이미지를 '5 아님' 클래스로 분류하는 더미 분류기를 만들어봅니다.



이렇게 5 아님 분류기를 만들어서, 정확도를 보면 90%가 모두 넘지만,

이미지의 10%가 5라는 점을 감안한다면 맞출 확률 또한 90%입니다. 

즉 불균형한 테이터셋을 다룰 때 '정확도'를 성능 지표로 선호하지 않는 이유를 알려줍니다.


2. 오차 행렬


두 번째로 분류기의 성능을 평가하는 더 좋은 방법은 오차 행렬(confusion matrix)을 조사하는 것입니다.

기본적인 아이디어는, 클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 것입니다. 

예를 들어, 분류기의 숫자 5의 이미지를 이미지 3으로 잘못 분류한 횟수를 알고 싶다면, 오차 행렬의 5행 3열을 보면 됩니다.


오차 행렬을 만들기 위해서는, 실제 타깃과 비교할 수 있도록 먼저 예측값을 만들어야 합니다.

테스트 세트로 예측값을 만들 수 있지만, 여기서 사용하면 안됩니다.(테스트 세트는 마지막에 사용된다는 점 기억!)

대신 cross_val_predict()함수를 사용할 수 있습니다.


cross_val_score() 함수처럼 cross_val_predict() 함수는 K-겹 교차 검증을 수행하지만 평가 점수를 반환하지 않고,

각 테스트 폴드에서 얻은 예측을 반환합니다. 즉 훈련 세트의 모든 샘플에 대해 깨끗한 예측을 얻게 됩니다.

(깨끗하다 = 모델이 훈련하는 동안 보지 못했던 데이터에 대해 예측했다는 의미)




이제 confusion_matrix() 함수를 사용해 오차 행렬을 만들 준비가 되었습니다.

타깃 클래스(y_train_5)와 예측 클래스(y_train_pred)를 넣고 호출하면 됩니다.



오차행렬의 행은 실제 클래스, 열은 예측한 클래스를 나타냅니다.


 

 '5 아님 이미지'


Negative Class

'5 이미지' 


Positive Class


'5 아님 이미지'


Negative Class


True Negative

진짜 음성

53,783 

False Positive

거짓 양성 

796


 '5 이미지'


Positive Class


False Negative

거짓 양성

1,950 

True Positive

진짜 양성 

3,471


즉, 5 아님 이미지에서 5가 아닌 것을 53,783개 구분해내고, 5가 아닌데 5라고 잘못 분류한 것이 796개입니다.

또, 5 이미지에서 '5가 아님'으로 분류한 것이 1,950개이며 5이미지를 5라고 잘 분류한 것이 3,471개라고 보면 됩니다.


이렇게 오차 행렬이 많은 정보를 제공해주지만, 가끔 더 요약된 지표가 필요할 때가 있습니다.

살펴볼만한 것 중 하나양성 예측의 정확도입니다. 이를 분류기의 정밀도(Precision)라고 합니다.



확실한 양성 샘플 하나만 예측하면, 간단히 완벽한 정밀도를 얻을 수 있지만, 이는 분류기가 다른 모든 양성 샘플을 무시하기 때문에

그리 유용하지 않습니다. 정밀도는 재현율(Recall)이라는 또 다른 지표와 같이 사용하는 것이 일반적입니다.

재현율은 분류기가 정확하게 감지한 양성 샘플의 비율로, 민감도(Sensitivity) 또는 진짜 양성 비율(True Positive Rate, TPR)이라고도

합니다.





정밀도는 81.3% 재현율은 64.0%가 나왔네요

즉, 5로 판별된 이미지 중 81.3%만 정확합니다. 게다가, 전체 숫자 5에서 64.0%만 감지했습니다.

정밀도와 재현율을 점수( score)라고 하는 하나의 숫자로 만들면, 편리할 때가 많습니다.

특히 두 분류기를 비교할 때 그렇습니다. 점수는 정밀도와 재현율의 조화 평균(harmonic mean)입니다.



아래의 식은 F 점수의 일반화된 조화 평균 식입니다. 

베타가 1보다 크면 재현율이 강조되고, 베타가 1보다 작으면 정밀도가 강조됩니다.

위의 식은 베타가 1일 때의 점수를 점수라고 합니다.




이렇게 점수를 내보았는데요, 정밀도와 재현율이 비슷한 분류기에서는 F1점수가 높습니다. 

하지만 이게 항상 바람직하지는 않고, 상황에 따라 정밀도나 재현율이 중요할 수 있습니다.


예를 들면, 어린 아이에게 안전한 컨텐츠만 보여지도록 걸러내는 분류기라면, 

좋은 동영상이 많이 제외되더라도 안전한 컨텐츠만 노출되는 ( 높은 정밀도 ) 분류기를 선호하게 됩니다.


또, 감시 카메라르르 통해 좀도둑을 잡아내는 분류기라면 재현율이 더 중요하게 될 지도 모릅니다.


하지만, 재현율과 정밀도 두 가지를 모두 가질 수 없는 것을 정밀도/재현율 트레이드오프라고 합니다.


SGDClassifier가 분류를 어떻게 결정하는지 살펴보며 이 트레이드 오프를 이해해봅니다.

SGDClassifier는 결정 함수(decision function)을 사용하여 각 샘플의 점수를 계산합니다.

이 점수가 임곗값보다 크면 샘플을 양성 클래스에 할당하고, 그렇지 않으면 음성 클래스에 할당합니다.



결정 임곗값이 가운데 화살표라고 가정하면, 임곗값 오른쪽에 4개의 5 (TP)가 있고 1개의 5 (FP)가 있습니다.

그렇기 때문에 이 임곗값에서 정밀도는 80%(4/5)입니다. 하지만 실제 숫자 5는 6개이고, 분류기는 4개만 감지했으므로

재현율은 67%(4/6)이 됩니다. 


만약, 임곗값을 높이면 거짓 양성이었던 6이 진짜 음성이 되어 정밀도가 높아집니다. (100%)

하지만 진짜 양성 하나가 거짓 음성이 되었으므로 재현율은 50%가 됩니다. 

반대로, 임곗값을 내리면 재현율이 높아지고, 정밀도가 줄어듭니다.



사이킷런에서 임곗값을 직접 지정하지는 못하지만, 우리가 예측에 사용했던 점수는 확인이 가능합니다.

분류기의 predict()메서드 대신 decision_function()메서드를 호출하면 각 샘플의 점수를 얻을 수 있습니다.

또, 이 점수를 기반으로 원하는 임곗값을 정해 예측을 만들 수 있습니다.


y_score에 some_digit (9)에 썼던 점수를 확인하면 -185971입니다. 

threshold를 0으로 지정하는 이유는 SGDClassifier의 임곗값이 0이기 때문입니다.

이렇게 다시 분류하면 predict()메서드와 같은 결과가 나옵니다.


임곗값을 높인다면 똑같이 False가 나오죠.

하지만 임곗값을 낮추면 재현율이 올라가고, 정밀도가 떨어지기 때문에 True로 바뀌는 모습을 볼 수 있습니다.

잘못 분류한 거죠.


또, 실제로 True였어도 임곗값을 높인다면 재현율이 떨어지기 때문에 False로 바뀔 수도 있습니다.


그렇다면, 적절한 임곗값을 어떻게 설정할 수 있을까요?

이를 위해서는 먼저 cross_val_predict()함수를 사용해 훈련 세트에 있는 모든 샘플의 점수를 구해야 합니다.

하지만 이번에는 예측 결과가 아니라 결정 점수를 반환 받도록 지정합니다.



이렇게 cross_val_precict에서 method="decision_function" 으로 지정해주면

y_scores에 결정 점수를 1차원 배열로 반환받습니다.



sklearn 0.19버전에서 cross_val_predict에서 method가 decision_function이면서 이진분류일 때,

1차원 배열이 아니고 2차원 배열로 반환하는 버그가 있습니다. 따라서 0.19.1버전으로 업그레이드 하거나

y_scores[:,1]처럼 두 번째 열만 사용해야 합니다.


하지만 저는 sklearn 0.20.1버전을 사용하고 있으므로 문제가 없습니다.

precisions, recall, threshold 모두 1차원 배열로 잘 나타나 있네요.

혹시 모르니까 shape와 ndim을 이용해서 확인 해봅니다.




문제가 없죠. 그래서 이제 matplotlib을 통해 그림을 그립니다.



xlim을 지정하여 조금 더 확대해서 볼 수도 있습니다.

그림을 보면, 정밀도 곡선이 재현율 곡선보다 왜 울퉁불퉁한지 의아할 수 있습니다.

이유는 임곗값을 올리더라도, 정밀도가 가끔 낮아질 수 있기 때문입니다. (일반적으로는 높아져야 함)

이처럼 정밀도가 올라가면 재현율이 내려가고, 재현율이 올라가면 정밀도가 떨어지는 Trade-off 관계를 볼 수 있습니다.


이제, 작업에 맞는 최선의 정밀도/재현율 트레이드 오프를 만드는 임곗값을 선택하면 됩니다.

재현율에 대한 정밀도 곡선을 그리면 좋은 정밀도/재현율 트레이드 오프를 선택하는 데 도움이 됩니다.



재현율 80% 근처에서, 정밀도가 급격하게 떨어지는 모습을 볼 수 있습니다.

이 하강점 직전을 정밀도/재현율 트레이드오프로 선택하는 것이 좋습니다.

예를 들면 재현율 60%정도 지점을 선택하는 것입니다.


정밀도 90%를 달성하는 것이 목표라고 가정하면, 위 그림에서 임곗값이 약 11만 정도 되는 것을 알 수 있습니다.

예측을 만들려면, 분류기의 predict()메서드를 호출하는 대신 다음 코드를 만듭니다.



이렇게 y_train_pred_90에 임곗값을 지정해주고 이 예측에 대한 정밀도와 재현율을 확인해보면

정밀도가 90% 가까이 나옴을 알 수 있습니다. 하지만, 재현율이 너무 낮아도 좋은 분류기라고 할 수 없기 때문에

임곗값을 낮춥니다.


83%까지 떨어졌지만, 재현율이 조금 올라갔네요.

여러 숫자를 넣어봤지만, 정말 Trade-off 관계에서 어떤 점이 최적으로 삼아야 할지 고민되는 부분일 것 같습니다.


이외에도, ROC곡선이 있습니다. 


ROC곡선은 수신기 조작 특성(Receiver Operating Characteristic) 곡선이라고도 하고, 이진 분류에서 널리 사용하는 도구입니다.

Precision/Recall 곡선과 유사하지만, ROC곡선은 정밀도에 대한 재현율 곡선이 아니고, 거짓 양성비율(false positive rate, FPR)에 대한 진짜 양성비율(true positive rate, TPR)의 곡선입니다. 

양성으로 잘못 분류된 음성 샘플의 비율이 FPR이고, 이는 1에서 음성으로 정확하게 분류한 음성 샘플의 진짜 비율인 진짜 음성비율(true negative rate, TNR)을 뺀 값입니다. TNR을 특이도(specificity)라고도 하고, 그러므로 ROC 곡선은 민감도(재현율)에 대한 1-특이도 그래프입니다.


FPR과 TNR과의 관계는 다음과 같습니다.



ROC곡선 = 민감도(재현율)에 대한 1-특이도 그래프

ROC 곡선을 그리기 위해서 roc_curve()함수를 사용하여 여러 임곗값에서 TPR, FPR을 계산해야 합니다.



물론, 여기서도 Trade-Off는 존재하는데요, 재현율(TPR)이 높을수록 분류기가 만드는 거짓 양성(FPR)이 늘어납니다.

그래프에서 점선이 나타내는 것은 랜덤 분류기의 ROC 곡선을 뜻하고, 랜덤 분류기란 훈련 데이터의 클래스 비율을 따라 무작위로 예측하는 것을 말합니다. 이렇게 되면 오차행렬의 실제 클래스가 비슷한 비율의 에측 클래스로 나뉘어 FPR, TPR 값이 비슷해집니다.

결국 ROC 곡선이 y=x에 가깝게 되어 AUC면적이 0.5가 됩니다. (사이킷런의 DummyClassifier에서 랜덤 분류기를 제공합니다.)


좋은 분류기는, 이 점선으로부터 최대한 멀리 떨어져있어야 하며(왼쪽 모서리), 곡선 아래의 면적(Area under the curve, AUC)을 측정하면 분류기들을 비교할 수 있습니다. 완벽한 분류기는 ROC의 AUC가 1이고, 완전한 랜덤 분류기는 0.5입니다.



이렇게 AUC를 구하면 0.95에 가깝습니다.


그렇다면, ROC곡선과 정밀도/재현율(PR) 곡선 중 어떤 것을 사용해야 할까요??

1. 일반적으로는 양성 클래스가 드물거나, 거짓음성보다 거짓양성이 더 중요할 때 PR곡선을 사용하고 그 외에는 ROC곡선을 사용핳ㅂ니다.


방금의 분류기 AUC 점수는 좋은 분류기라고 생각할 수 있지만, 이는 '음성 아님(5 아님)'에 비해 양성(5)이 크게 적기 때문입니다. 이와는 다르게 PR곡선은 분류기의 성능 개선 여지가 얼마나 되는지 (오른쪽 위 모서리로 가까워질 수 있는지) 보여줍니다.


이제, RandomForestClassifier를 훈련시켜서 SGDClassifier의 ROC곡선과 AUC점수를 비교해보겠습니다.

먼저, 훈련 세트 샘플에 대한 점수를 얻어야 합니다. 하지만, 작동방식의 차이 떄문에, 

RandomForestClassifier에는 decision_function()메서드가 없고,  predict_proba()메서드가 있습니다. 

일반적으로 사이킷런 분류기는 둘 중 하나 이상은 가지고 있습니다.


predict_proba() 메서드는 샘플이 행, 클래드가 열이고, 샘플이 주어진 클래스에 속할 확률을 담은 배열을 반환합니다.

 (어떤 이미지가 5일 확률 70% 같이)



앞서 말했던 것 처럼 predict_proba()는 확률을 담은 배열을 반환하기 때문에 ROC곡선을 만들기 위한 점수를 얻어야 합니다.

간단한 방법은, 양성 클래스의 확률을 점수로 사용하는 것입니다.


y_scores_forest=y_probas_forest[:,1] 로 양성 클래스에 대한 확률을 점수로 사용하고, ROC곡선으르 첫 SGD곡선과 같이 그립니다.

그래프를 보면, RandomForestClassifier의 ROC곡선이 왼쪽 위 모서리에 가까워 SGDClassifier보다 훨씬 좋아보입니다.



roc_auc_score(y_train_5, y_scores_forest)로 AUC점수를 계산해보면, 1에 매우 가까운 0.993정도를 얻었음을 알 수 있습니다.

이렇게 다양한 지표들을 알아보았는데요. 다음엔 다중분류에 대해 알아보도록 하겠습니다.


 블로그 

출처


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


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

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


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

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



반응형