03_Classification.ipynb

책: O'REILLY의 Hands-On Machine Learning with Scikit-Learn & TensorFlow

URL: http://shop.oreilly.com/product/0636920052289.do

본문에 사용되는 사진들은 모두 위의 책에서 발췌

소스코드: https://github.com/ageron/handson-ml

틀린 내용이 있으면 언제든지 댓글 달아주세요! (오역은 어느정도 이해해주시길)


chapter3. Classification

chap1에서는 가장 일반적은 지도학습은 회귀(regression)와 분류(classification)이라고 언급했다.

chap2에서는 선형 회귀(Linear Regression), 결정 트리(Decision Trees), 랜덤 포레스트(Random Forests) 알고리즘을 이용하여 주택 가격을 예측하는 회귀 문제를 경험했다.

이제 우리는 분류 시스템에 대해 주목하고자 한다.


MNIST

 이번 장에서 우리는 MNIST 데이터를 이용할 것이다. MNIST데이터는 0에서 9까지의 숫자를 70,000장의 수작업으로 작성 된 데이터 셋이다. 각 이미지는 0에서 9까지의 숫자에 대해 라벨링 되어 있다. 이 데이터 셋은 너무나 많이 연구되어 머신러닝의 "hello world"라고도 불리며, 사람들이 새로운 분류 알고리즘을 적용할 때마다 MNIST에서 어떻게 작동되는지를 알고 싶어한다.

Scikit-learn은 인기있는 데이터 셋을 다운로드 할 수 있는 기능을 제공하는데 그 중 하나가 MNIST이다. 


 

 다운로드 받은 Datasets은 일반적으로 사전식(dictionary) 구조와 유사하다.

  • DESCR key: 데이터셋의 설명(description)
  • data key: 인스턴스당 하나의 행과 feature당 하나의 열이 있는 배열
  • target Key: 라벨을 가진 배열

 총 70,000장의 이미지는 784개의 feature를 가지고 있다. 각각의 이미지는 28*28 픽셀을 가지고 있으며, 각각의 feature는 0(white)에서 255(black)까지의 하나의 픽셀로 나타낼 수 있다. 


 설명만으로는 감을 잡기 어려울 것이다. 이를 실제로 28*28 행렬로 변환하여 이미지를 출력해보자.

0에서 69,999까지의 범위 중 랜덤 값으로 하나를 출력하여 이미지를 출력한 결과이다. 실제 타겟의 값이 8이고, 이미지 또한 8이라는 것을 "우리는" 확인 할 수 있다.

 이제 우리는 2장에서 배운 학습용 데이터와 테스트용 데이터를 분리 해야한다. MNIST 데이터 셋은 실제로 학습데이터(첫 번째 데이터에서 60,000까지의 이미지)와 테스트 데이터(나머지 10,000장의 이미지)로 이미 분리되어 있다.

 또한, 학습용 데이터를 섞어보자(shuffle). 몇몇 알고리즘은 인스턴스의 순서에 민감하다. 그렇기 때문에 섞는 작업을 통해 이러한 문제를 해결할 수 있다.


Training a Binary Classifier

 단순한 문제를 풀어보자. 예를 들어 숫자 5를 식별하는 분류기를 만들어보자. 이 분류기는 단순히 5가 맞다, 아니다의 두가지 클래스만을 가지고 있다. 이 분류 작업을 위해 타겟 벡터를 만들어보자.

 이제 학습을 시켜보자. 시작하기 좋은 방법으로 Scikit-learn의 SGDClassifier 클래스를 이용하여, Stochastic Gradient Descent(SGD) 분류기를 이용하여 시작해보자.

 [Stochastic Gradient Descent 방식은 기존의 경사 하강법(Gradient Descent)이 매 번 모든 데이터들에 대해 살펴보고 기울기를 계산하기 때문에 시간이 많이 소요되는 문제가 있었는데, 이를 개선한 것이다. 책에서 해당 내용에 대해 다루는지 아직 파악이 안되었는데, 다루지 않을 경우 별도로 정리하겠음.]

 이 SGD 분류는 매우 큰 데이터 셋을 다루는 데 매우 효율적이다. 아래의 예제는 some_digit을 X[36000]으로 맞춰 놓은 상태

 이 분류기는 특정한 상황에서만 올바르게 판단된다. 이제 우리는 모델의 성능에 대해 평가해보고자 한다.


1. Performance Measures

 분류기를 평가하는 것은 종종 regressor를 평가하는 것보다 훨씬 더 까다로울 수 있다. 그래서 우리는 이 주제에 대해 이번 장에서 많은 부분을 할애하고자 한다. 사용할 수 있는 성능 측정 방법은 상당히 많다.


1) Measuring Accuracy Using Cross-Validation

 모델을 평가하는 좋은 방법으로는 교차 검증(cross-validation)을 이용하는 것이다.

경우에 따라서, Scikit-learn이 제공하는 것보다 교차-검증을 더욱 제어해야 할 수 있다. 

이러한 경우 쉽게 구현할 수 있다.

다음의 코드는 Scikit-learn의 cross_val_score() 함수와 거의 동일한 결과를 보인다.


 이번에는 cross_val_score() 함수를 사용하여, 3개의 fold를 가진 K-fold cross-validation을 이용한 SGDClassifier 모델을 평가해보자. 


 Wow! 95% 이상의 정확도를 보인다? 

흥분하기 전에, "not-5"클래스의 모든 단일 이미지를 분류하는 멍청한(dumb) 분류기를 살펴보자.

 그렇다. 90%의 정확도를 보인다. 5라는 이미지가 대략 10%정도 차지하고 있기 때문에 나오는 간단한 결과이다.


위 예는 분류를 측정하는, 특히 왜곡(skewed)된 데이터를 다룰 때 선호되는 척도가 아닌지에 대해 증명하고 있다.


2) Confusion Matrix

 분류기의 성능을 평가하는 더 좋은 방법은 confusion matrix를 사용하는 것이다. 일반적인 아이디어는 클래스 A의 인스턴스가 클래스 B로 분류되는 수를 계산하는 것이다. 예를 들어, 5라는 이미지를 3으로 혼동하는 횟수를 알기 위해서는, confusion matrix의 5행 3열을 보면 된다. confusion matrix를 계산하기 위해, 처음으로 예측 Set을 구하고, 그 다음 실제 타겟과 비교하면 된다. test set에서 예측을 할 수 있지만, 일단은 그대로 두자. (test set은 학습을 마치고, 나중에 평가할 때 사용하기 위해 아껴두는 것을 잊지말자!) 대신 cross_val_predict() 함수를 사용할 수 있다. 

 cross_val_score() 함수와 마찬가지로, cross_val_predict() 함수는 K-fold cross-validation을 수행한다. 그러나 evaluation scores값을 반환하는 것 대신에 각각의 test fold에서 만들어지는 예측에 대해서 반환하게 된다. 

 이제 confusion_maxtrix() 함수를 이용하여 confusion matrix를 얻어보자. 

  confusion matrix의 각 행은 actual class를 나타내고, 각 열은 predicted class를 나타낸다. 이 행렬의 첫번째 행은 5가 아닌 것(negative class)을 53,800개로 분류하고(true negatives), 나머지 779개는 5로 잘못 분류한 것이다(false positives).

 다음 행은 5인 것(positive class)을 분류하는 것인데, 1,583개가 잘못 분류 되었지만(false negatives), 나머지 3,838개는 정상적으로 분류 되었다(true positives).

 글로 보려니 아무래도 헷갈린다! 그리고 무엇보다 헷갈렸던 것은 알고 있던 confusion matrix는 행이 예측 클래스, 열이 actual 클래스였는데, scikit-learn에서 반대로 출력되어 헷갈렸다..

 나는 매번 헷갈렸지만, 그래도 그나마 외우기 위한 방법으로는 다음과 같은 방법을 이용했다. 다소 유치할 수는..

"예측을 했는데 5가 아니었네. 실제는 뭐지? 아! 5가 아니네! True Negatives = (뒤에서부터) 아닌게 맞구나~"

"예측을 했는데 5네! 실제는 뭐지? 이런..5가 아니었네... False Positives = (뒤에서부터) 예측이 맞은게 아니었구나.."

"예측을 했는데 5가 아니었네. 실제는 뭐지? 아! 5군... False Negatives = (뒤에서부터) 아닌게 아니었구나.."

"예측을 했는데 5네! 실제는 뭐지? 오! 5가 맞았군! True Positives = (뒤에서부터) 맞춘게 맞구나~"


 confusion matrix는 많은 정보를 줄 수 있지만 때로는 더욱 간결한 것을 원할 수 있다. 이 때 사용되는 것으로는 precision(정밀도)과 recall(재현율) 이 있다.

 정밀도란, 맞다고 예측한 것실제로 맞았는지에 대해 확인하는 지표이다.

 재현율이란, 실제 정답의 True얼마나 많은 true를 예측 했는지에 대해 확인하는 지표이다.


3) Precision and Recall

 scikit-learn은 precision과 recall을 포함하여 classifier metrics 를 계산하는 몇개의 함수를 제공한다.

 이제 숫자 5의 검출기는 이전의 높은 정확도를 보이는 것처럼 높게 나오지는 않는다. 이미지가 5일 때, 83%정도 맞다고 주장한다. 더욱이 5의 70%만 탐지하게 된다.

두 분류기준을 비교하는 간단한 방법이 필요할 때, F1 score라고 불리는 (Precision과 recall을 하나의 metric으로 결합)방법을 사용할 수 있는데, 우리가 알고 있는 조화 평균이 해당 방법이다.

F1 score를 계산하기 위해, 간단하게 f1_score() 함수를 호출하면 된다.


그런데 F1 score 값은 항상 우리가 원하는 값만 출력하는 것은 아니다. 상황에 맞게 precision과 recall 중 중점적으로 생각할 수 있을 것이다.

 예를 들어, 어린이에게 안전한 동영상을 볼 수 있도록 분류기를 훈련하는 가정을 생각해보자. 이러한 경우, 많은 좋은 동영상을 나쁘다고 판단할 수 있다(low recall). 하지만, 많은 분류 기준을 사용하는 분류 기준보다는 안전하다고 판단되는 분류 기준을 선호하게 된다(high precision).

 반면에, 감시 이미지에서 좀도둑을 탐지하도록 분류기를 훈련시킨다고 가정해보자. 이 때에는 precision보다는 recall이 높은게 좋다. 잘못된 알람(맞다고 예측했는데 아닌 경우: low precision)이라고 해도 (경비원들은 엄청 고생하시겠지만..) 실제 도둑을 탐지하는 비율이 높게 된다. 

 위의 두 가지 예를 보면 precision과  recall은 반비례적인 것을 확인 할 수 있다. 이를 Precision/recall tradeoff라고 부른다.


4) Precision/Recall Tradeoff

 tradeoff를 이해하기 위해서, SGDClassifier가 어떻게 분류 결정을(classification decisions) 하는지 살펴보자. 각각의 instance에 대해 decision function 기반으로 점수를 계산하고, 만약 score가 임계 값(threshold)보다 높다면 positive class에 할당하고, 그렇지 않다면 negative class에 할당한다. 

위의 그림은 왼쪽으로의 낮은 점수부터 오른쪽의 높은 점수에 이르기까지 몇개의 숫자를 보여준다. 결정 임계값(decision threshold)를 중간 화살표(5사이)로 가정해보자. 오른쪽을 보면 실제 5라는 숫자가 4개라는 것을 확인할 수 있다. 6은 무엇일까?

앞서 얘기한 False Positive가 될 것이다.(예측은 5였는데 실제값은 5가 아니었군!) 따라서 해당 임계 값을 사용하면 precision은 80%가 될 것이다. TP/(FP+TP) -> 4/(1+4). 그러나 실제 5의 개수는 6개인데 해당 임계 값의 범위에는 5가 4개밖에 포함되어 있지 않다. 그렇기 때문에 recall은 67%가 된다. TP/(TP+FN) -> 4/(4+2)

 이제 임계값을 오른쪽 화살표로 움직여보자. 이 때 precision은 100%가 된다.  TP/(FP+TP) -> 3/(0+3). 그러나 recall은 50%로 감소하게 된다. 3/(3+3). 즉 precision은 높아졌지만, recall은 감소한 것을 확인할 수 있다. 왼쪽으로 화살표를 가면 반대라는 것 또한 확인할 수 있다.

 scikit-learn은 임계값을 직접 설정할 수는 없지만, 예측을 하기 위해 사용하는 의사결정점수(decision score)에 대한 접근을 제공한다. 분류기(classifier)의 predict() 함수를 호출하는 것 대신에, 각각의 인스턴스의 점수를 반환하는 decision_function() 함수를 호출하여 원하는 임계 값을 사용하여 해당 점수를 기반으로 에측을 수행할 수 있다. 

 SGDClassifier는 threshold를 0으로 사용하기 때문에 이전 predict() 함수를 사용했던 결과와 동일하게 True 값을 반환한다. 

이제 임계값을 올려보자. 200,000으로 값을 줄 것이다.

 당연히 y_score 값이 74632었기 때문에 false가 반환된다. 이것이 중요한게 아니라, 임계 값을 높이면 recall이 감소한다는 것이다. 이미지는 실제로 5를 나타내며 분류기는 임계 값이 0일때 5가 맞다는 것을 감지하지만 임계값이 200,000으로 증가하면 탐지 하지 못하게 된다. 그러면 임계값을 어떻게 결정할 수 있을까? 이를 위해서 우리는 cross_val_predict() 함수를 다시 사용하여 training set의 모든 인스턴스 점수를 얻을 것이고, 이번에는 예측하는 것 대신에 의사 결정 점수를 반환하도록 지정할 것이다.

(예측에는 false, true 형식으로 반환되지만, 결정 점수는 값으로 반환된다ㅋ)

 이제 이 점수로, precision_recall_curve() 함수를 사용하여 가능한 모든 임계값에 대해 precision과 recall을 계산할 수 있다.

 한가지 주의해야할 점은 sklearn의 0.19버전의 경우 cross_val_predict함수의 method로 decision_function의 반환 shape는 (60000,2)다. y_train의 경우 (60000,1)이기 때문에 형식이 맞지 않는다며 오류값이 나온다. 그렇기 때문에 위에서 y_scores[:,1]로 했다는 점에 대해 유의 필요.

 위의 그래프에서 precision이 recall보다 왜 왜곡되었는지 궁금해 할 수 있다. (recall 커브가 부드럽게 보인다. )

이유는 임계 값을 올릴 때 정확도가 떨어질 수 있기 때문이다.  위의 precision/recall tradeoff 그림을 보면 설명이 되는데, 중간 화살표인 임계값을 보자. 임계값에서 한 자리수만큼 오른쪽으로 이동시켜보자. 정밀도는 4/5(80%)에서 3/4(75%)라는 것을 확인 할 수 있다. 반면에 recall은 임계 값이 증가 할 때만 값이 내려가는 것을 확인할 수 있다. 그렇기 때문에 커브가 부드럽게 보이는지 설명할 수 있다. 

 위의 방법으로 최적의 precision/recall tradeoff 값을 제공하는 임계값을 간단하게 선택할 수 있게 되었다.  

또 다른 방법으로는 recall에 대한 precision을 직접 plotting 하는 것이다.

 위의 그래프를 보면 precision은 80%가량에서 급격히 떨어지는 것을 확인할 수 있다. 


이제 90%의 precision을 목표로 한다고 가정해보자. threshold를 통한 precision/recall 그래프를 다시 보자. (위의 위의 그래프)

만족하는 threshold는 약 70,000 정도로 보인다. 

70,000을 기준으로 threshold에 값을 준 결과, precision은 90%인 것을 확인 할 수 있다. 참 쉽죠잉~


5) The ROC Curve (Receiver Operating Characteristic)

 ROC Curve는 precision/recall curve와 매우 유사하지만, recall에 대한 precision을 plotting 하는 것 대신, False Positive Rate(FPR)에 대해 True Positive Rate(TPR; recall)을 plotting 하는 것이다. 

 설명이 어려웠다. 그냥 precision/recall은 precision과 recall이 반비례 관계이고, ROC의 경우 TPR과 FPR이 반비례다.

이것을 설명하기 위해서는 민감도(sensitivity)와 특이도(specificity)에 대해 알아야 된다.

1. 민감도(sensitivity): 1인 케이스에 대해 1이라고 예측

2. 특이도(specificity): 0인 케이스에 대해 0이라고 예측

True Positive Rate(TPR)는 민감도와 같다. recall과 같다. (ex: 스팸 메일을 스팸메일이라고 판단함)

False Positive Rate(FPR)는 (1-특이도)와 같다. 즉 0인 케이스에 대해 1로 잘못 예측한 비율이다.
(ex: 스팸 메일이 아닌데 스팸 메일이라고 잘못 판단함)

True Negative Rate(TNR)은 특이도와 같다. (ex:스팸 메일이 아닌 것에 대해 정확하게 판단함)

즉 FPR은 (1-TNR)이다.


 ROC curve를 plot하기 위해, 우선 roc_curve()함수를 이용하여, 다양한 임계값을 통해 TPR과 FPR을 계산하여야 한다.

 recall(TPR)이 높을 수록 분류기가 생성하는 오탐(FPR)이 높아진다. 점선은 purely random classifier를 나타낸다. 좋은 분류기 일수록, 해당 선에서 멀리 떨어진다(왼쪽 상단 모서리 방향).

 분류 기준을 비교하는 한가지 방법으로 AUC(Area Under the Curve)를 측정이 있다. 완벽한 분류기는 ROC AUC의 크기가 1인 반면, purely random classifier는 0.5가 된다. scikit-learn은 ROC AUC를 계산하는 함수를 제공한다.


Tip

 ROC 커브와 Precision/recall 커브가 매우 유사하여, 어떤 것을 언제 써야되는지에 대해 결정하는 방법에 대해 궁금할 수 있다. 만약, positive class가 드물거나, False Negatives보다 False Positives에 대해 유의해야 되는 상황이라면, precision/recall 커브를 사용해야 한다. 그렇지 않은 상황이라면 ROC 커브를 사용한다. 

 예를 들어, 위의 roc_auc_score에서 0.95라는 점수를 보면 실제로 좋다고 생각 할 수 있다. 그러나 이것은 대부분 negative(5가 아닌)에 비해 positive가 너무 적기 때문이다.


 지금까지는 바이너리 즉 5와 5가 아닌 것에 대해 분류기를 훈련하였다. 

분류기의 성능을 측정하기 위해 교차검증(cross_validation)을 이용하였고, 필요에 따라 Precision/recall 곡선과, ROC 곡선 및 ROC AUC 점수를 통해 모델을 비교하는 방법을 알았다. 

 이제는 5와 5가 아닌 것에 대해 판단하는 것이 아닌, multi class 분류 방법에 대해 살펴보고자 한다.


Multiclass Classifier

 바이너리 분류기가 두 클래스를 구별하는 방식이라면, 멀티 클래스 분류기(다항)는 두 개 이상의 클래스를 구별하는 방식이다. 

Random Forest classifiers 나 naive Bayes classifiers같은 몇몇 알고리즘은 직접적으로 멀티 클래스를 다룰 수 있다.

그 외 Support Vector Machine이나 Linear classifiers과 같은 알고리즘은 엄격하게 바이너리 분류 방법이다. 그러나 멀티 바이너리 분류기를 이용하여 멀티 클래스 분류기를 수행할 수 있는 다양한 방법들이 존재한다.

 예를 들어, 숫자 이미지를 10개의 클래스(0에서 9까지)로 분류할 수 있는 시스템을 생성하는 방법 중 하나는, 10개의 바이너리 분류기를 각 숫자(0-탐지기, 1-탐지기 등)에 대해 하나씩 훈련 시키는 방법이 있다. 그다음 이미지를 분류하길 원할 때, 각 분류기로부터 decision score를 얻고, 가장 높은 점수를 출력하는 클래스를 선택할 수 있다. 이것을 one-versus-all(OvA) 전략이라고 한다. (또는 one-versus-the-rest)

 다른 전략으로는 모든 숫자의 쌍(pair of digits)에 대해 바이너리 분류기를 훈련시키는 방법이 있다. 0과 1을 구별하고, 0과 2를 구별하고, 1과 2를 구별하고 이를 모든 숫자에 대해 쌍으로 만드는 것이다. 이는 1대1(OvO) 전략이라고 한다. N개의 클래스가 있다면 N*(N-1) / 2 개의 분류기를 학습해야 한다. (숫자 10개이기 때문에 45개를 학습해야한다 OTL..) 이미지를 분류하려면 45개의 모든 분류기를 통해 어떤 클래스가 가장 많이 차지하는지 확인 해야 된다. OvO의 가장 큰 장점은 각각의 분류기를 구분하고자 하는 두개의 클래스를 위해 training set의 부분에 대해서만 학습하면 된다는 장점이 있다. (즉 45개의 분류기일지라도 1과 2를 분류하는 분류기의 경우 1과 2의 데이터에 대해서만 학습하면 된다) 

 Support Vector Machine 분류기와 같은 일부 알고리즘은 training set의 크기에 따라 가늠할 수 없기 때문에, 대규모의 training sets에서 조금의 분류기를 학습하는 것 보다 작은 training sets에서 많은 분류기를 훈련시키는 것이 빠르기 때문에 OvO 방식이 선호된다. 그러나 대부분의 바이너리 분류 알고리즘은 OvA가 선호된다.

 scikit-learn은 멀티 클래스 분류 작업에 바이너리 분류 알고리즘을 사용하려고 할 때 이를 감지하고 자동으로 OvA를 실행한다. (OvO를 사용하는 SVM 분류를 제외하고는)

 SGDClassifier를 이용해보자.

 매우 간단하다. 이 코드는 숫자 5랑 모든 타겟 클래스(y_train_5) 대신에 0에서부터 9까지의 original target 클래스인 y_train을 이용하여 학습하였다.  그런 다음 예측을 하였다. 

 scikit-learn은 실제로 10개의 바이너리 분류기를 학습하고, 이밎에 대해 decision scores를 얻었으며, 가장 높은 점수의 클래스를 선택하였다. 실제로 이것이 사실인지 확인하려면 decision_function() 함수를 호출하면 된다. 인스턴스 당 하나의 점수를 반환하는 대신 클래스 당 10개의 점수를 반환한다. 0부터 시작하기 때문에 6번째 값인 5가 가장 높은 점수임을 확인할 수 있다.

 만약 OvO 또는 OvA를 사용하고 싶다면 OneVsOneClassifier나 OneVsRestClassifier 클래스를 사용하면 된다. 

간단히 인스턴스를 생성하고 바이너리 분류기를 생성자에 보내주면 끝이다. 예를 들어, 아래의 그림은 SGDClassifier를 기반으로 OvO 전략을 이용한 멀티 클래스 분류기를 만든 것이다.

 이제는 배운 내용을 적용해보자. 분류기를 생성한 다음에는 무엇을 해야할까? 그렇다. 평가를 해봐야 된다. 이를 위해 교차 검증(cross_validation)을 적용해보자. cross_val_score() 함수를 사용하여 SGDClassfier의 정확도(accuracy)을 평가해보자.


 모든 test fold에서 85% 이상의 값이 나왔다. 정확도를 향상시킬 수는 없을까? 당연히 있다.

우리는 2장의 4-4에서 feature scaling에 대해 배우지 않았던가. 입력 값을 scaling 해보자.

단순 표준화(standardization)를 통해 성능이 90%이상까지 끌어 올릴 수 있었다.


Error Analysis

 우리는 제법 괜찮은 모델을 발견하였다고 가정해보자. 이제 이 모델을 향상시킬 방법을 찾고 싶다. 이를 위한 방법 중 하나는 오류 유형을 분석하는 것이다.  

 우선적으로, 앞 부분에 배웠던 confusion matrix를 살펴보자. 이전에 했던 것 처럼 cross_val_predict() 함수를 이용하여 예측을 수행 한 후, confusion_matrix() 함수를 호출 해야 한다.

숫자가 너무 많아 보기 힘들다. matplotlib의 matshow() 함수를 이용하여 confusion matrix의 이미지 표현을 보는 것이 더 편리하다. (대각선 값들이 심상치 않아보인다..)

위의 confusion matrix는 대부분의 이미지가 대각선에 위치하고 있기 때문에 상당히 잘 보인다. 즉, 올바르게 분류 되었다는 것을 의미한다. 5는 다른 자릿수보다 약간 더 어둡게 보인다. 이는 dataset에서 5의 이미지가 적게 있거나, 또는 분류기가 다른 숫자만큼 5에서 제대로 수행되지 않음을 의미할 수 있다. 실제로 둘 다 사실인지 확인할 수 있다. 

 오류에 관한 그림을 집중적으로 살펴보자. 먼저 confusion matrix의 각 값을 해당 클래스의 이미지 수로 나누어, 에러율을 비교할 수 있다. 

기억하자. 행은 실제 클래스를 나타내고 열은 예측 클래스를 나타낸다. 클래스 8과 9의 열은 꽤 밝은 것을 확인할 수 있다. 이는 8과 9를 잘 분류하지 못한다는 것을 의미한다. 마찬가지로, 클래스 8과 9의 행 또한 밝은 것을 확인 할 수 있는데, 이 또한 8과 9를 다른 숫자들과 종종 혼동이 된다는 것을 의미한다.

 이와는 반대로, 클래스 1의 경우, 어두운 것을 확인할 수 있다. 이는 대부분의 1이 정확하게 분류 된다는 것을 의미한다. 

오류는 완벽하게 대칭적은 아니다. 예를 들어, 실제는 5인데 예측은 8로 잘못 분류 된 것이 그 반대의 경우보다 더 많다.


 위의 두 개의 5*5 행렬은 3으로 분류된 것을 표시하고, 아래쪽의 두 개는 숫자 5로 분류 된 이미지이다. 

왼쪽 하단과 오른쪽 상단의 경우, 사람이 구분하는 것에도 문제가 있어보인다. 예를 들어 8행 1열의 5는 3처럼 보인다.
분류기가 잘못 분류를 한 이유를 이해하는 것은 상당히 어렵다. 그 이유는 선형 모델인 간단한 SGDClassifier 모델을 사용했기 때문이다. 클래스마다 가중치를 각 픽셀에 할당하고, 새로운 이미지를 확인할 때 가중치 픽셀 강도를 합산하여 각 클래스의 점수를 얻는다. 그렇기 때문에 3과 5는 단지 몇 픽셀만 다르기때문에 해당 모델은 쉽게 혼동 될 수 밖에 없다. 
 3과 5의 가장 큰 차이점은 하단의 arc(3과 5의 밑에부분인 둥근 부분)와 상단의 line(3과 5의 맨 위 선)을 연결하는 작은 선의 위치이다. 

 이 분류 기준은 이미지 이동 및 회전에 매우 민감하다. 따라서 혼란을 줄이는 한가지 방법은 이미지를 전처리하여 이미지가 중심에 있도록하고, 회전되지 않도록 하는 것이다. 이렇게 하면 다른 오류도 줄일 수 있다. 


Multilabel Classification

 지금까지의 각 인스턴스들은 하나의 클래스에만 할당 되었다. 경우에 따라 분류기는 각 인스턴스들에 대해 여러 클래스를 출력하도록 할 수 있다. 예를 들어 얼굴 인식 분류에 대해 생각해보자. 페이스북에서 단체 사진을 찍었다고 생각해보자. 이 사진에서 여러 사람을 인식해야 한다면 어떻게 해야할까? 물론 각각의 사람마다 하나의 라벨은 주어져야 된다. 분류기가 alice, bob, charlie의 얼굴을 인식하도록 학습했다고 가정해보자. 앨리스와 찰리의 사진만 있다면 [1,0,1]를 출력해야 된다. 이러한 multiple binary 라벨이 출력되는 분류 시스템은 multilabel classification system 이라고 부른다. 

 간단한 예를 살펴보자.

위의 코드는 각 자리수 이미지에 대해 두개의 타겟을 포함하는 multilabel 배열을 만든다. 첫 번째는 , 7 이상인 수인지 아닌지에 대해 나타내며, 두 번째는 홀수인지 짝수인지에 대해 나타낸다. 이 두 개를 합칠 때 np.c_ 를 사용하여 2열로 만든다.

 그 다음 KNeighborsClassifier 인스턴스를 만들고, multiple targets 배열을 이용하여 학습한다. 이제 우리는 예측을 할 수 있으며, 두 개의 라벨(label)이 출력 된다는 것을 알 수 있다.

위의 some_digit은 위에서부터 계속 사용했던 숫자 5다. 그렇기 때문에 앞의 False는 7이상이 아니기 때문에 출력된 것이며, 뒤의 True는 5가 홀수이기 때문에 true로 출력된다.

 

 multilabel 분류기를 평가하는 방법은 많이 있으며, 실제로 프로젝트에 따라 측정 항목을 선택하는 것도 다르다. 
예를 들어, 한가지 방법으로 각각의 라벨에 대한 F1 score를 측정한 다음, 간단하게 평균 점수만 계산하는 것이다. 아래 코드는 모든 라벨에서 F1 score의 평균을 계산한 것이다.

 이것은 모든 라벨이 똑같이 중요하다고 가정한 것이다. 특히, 만약 alice의 사진이 bob이나 charlie보다 많을 경우, alice의 사진에서 분류기의 점수에 더 많은 가중치를 주는 것이 좋다. 한가지 간단한 옵션은 각각의 라벨에 동일한 가중치(target label을 갖는 인스턴스의 수)를 부여하는 것이다. 이렇게 하려면 위의 코드에서 average = "weighted"를 설정하면 된다.

  • macro 평균은 클래스별 f1 score에 가중치를 주지 않는다. 클래스 크기에 상관없이 모든 클래스를 같은 비중으로 다룸

  • weighted 평균은 클래스별 샘플 수로 가중치를 두어 f1 score의 평균을 계산한다.이 값이 분류 report에 나타나는 값이다.

  • micro 평균은 모든 클래스의 FP(False Positive), FN(False Negative), TP(True Positive)의 총 수를 헤아린 다음 precision, recall, f1 score를 수치로 계산한다.

[파이썬 라이브러리를 활용한 머신러닝 책에서 macro/weighted/micro 평균 내용 발췌]
각 샘플을 똑같이 간주한다면 micro 평균을, 각 클래스를 동일한 비중으로 고려한다면 macro 평균 점수를 추천


Multioutput Classification

 이 장에서 논의 할 마지막 유형의 분류 작업은 multioutput-multiclass classification(또는 단순히 multioutput classification)이다. 이것은 간단히 각 라벨이 multiclass를 가질 수 있는 multilabel classification의 일반화이다. (즉 두 개 이상의 값을 가질 수 있음)

 이를 설명하기 위해 이미지로부터 노이즈를 제거하는 시스템을 만들어 보자. 입력 값으로 잡음이 많은 숫자 이미지를 선택 할 것이고, MNIST 이미지처럼 픽셀의 배열로 표현하는 숫자 이미지를 출력 할 것이다. 

 분류기의 출력 값은 multilabel(픽셀 당 하나의 라벨)이고, 각각의 라벨은 multiple 값 (픽셀 범위는 0에서 255)을 가질 수 있다. 

 

 먼저 Numpy의 randint() 함수를 사용하여 MNIST 이미지를 가져오고, 픽셀에 노이즈를 추가하여 학습 및 테스트 셋을 만들어보자. target 이미지는 원본 이미지이다.

 테스트 set으로부터 이미지를 살펴보자. 왼쪽에는 noisy 입력 이미지가 있고, 오른쪽은 타겟 이미지가 있다.

이제 분류기를 학습시키고 이 이미지를 clean하게 만들자.

 목표에 충분히 근접해 보인다. 이것으로 분류에 대한 결론을 마친다. 

본 3강을 통해, 이제 분류 작업에 적합한 metrics를 선택하고, 적절한 precision/recall tradeoff를 고르고, 분류기를 비교하는 방법을 알고, 좋은 분류 시스템을 구축할 수 있어야 된다.


하지만 아직 갈 길이 멀어 보인다.... 갈수록 어려워 지는 것 같은 기분이 들었다.....

4강에서는 모델을 학습하는 방법에 대해 다룬다.

댓글을 달아 주세요

02_End-to-End_Machine_Learning_Project(final).ipynb

책: O'REILLY의 Hands-On Machine Learning with Scikit-Learn & TensorFlow

URL: http://shop.oreilly.com/product/0636920052289.do

본문에 사용되는 사진들은 모두 위의 책에서 발췌

소스코드: https://github.com/ageron/handson-ml

틀린 내용이 있으면 언제든지 댓글 달아주세요! (오역은 어느정도 이해해주시길)


chapter2. End-to-End Machine Learning Project

 이번 장에서는 머신러닝의 프로젝트의 처음에서 끝까지 살펴보고자 한다. 주요 단계로는 아래와 같다.

  1. Look at the big picture.
  2. Get the data.
  3. Discover and visualize the data to gain insights.
  4. Prepare the data for Machine Learning algorithms.
  5. Select a model and train in.
  6. Fine-tune your model.
  7. Present your solution.
  8. Launch, monitor, and maintain your system.

Working with Real Data

 머신러닝을 공부하는데 있어 가장 좋은 방법은, 인공적으로 만든 데이터 셋이 아닌 실제 데이터를 가지고 실험하는 것이다. 우리가 사용할 수 있는 데이터는 상당히 많다. 

 이번 2장에서는 캘리포니아 주택 가격에 대한 데이터를 사용할 것이다. 해당 데이터는 1990년 캘리포니아 인구조사로부터 기반한다. (좀 오래 된 데이터이긴 하다..) 하지만 저자는 해당 데이터가 학습을 위해 많은 양을 포함하고 있고, feature에 대해 새로 추가하고 삭제하고 하는 기법을 사용한다고는 한다.

 

1. Look at the Big Picture

 Welcome to Machine Learning Housing Corporation!

 첫 번째 과제로는 캘리포니아 인구조사 데이터를 가지고 캘리포니아의 주택 가격의 모델을 만드는 것이다.

데이터로는 인구, 수입의 중간 값(median), 주택 가격의 중간 값(median), 그리고 캘리포니아의 지구(구역) 등이 측정 항목으로 포함되어 있다.

여기에서 지구(구역)은 'districts'로 약 600명에서 3,000명의 인구로 구성된 구역이다.

 만들어야하는 모델은 주어진 모든 측정 항목으로부터, 다른 districts의 주택 가격의 중간 값(median)을 예측할 수 있어야 된다.


1) Frame the Problem

 상사에게(지금 현재 Machine Learning Housing Corporation에 들어

와있다...) 가장 먼저 해야할 질문으로는 사업 목적에 대해 정확히 파악하는 것이다. 단순 모델을 만드는 것이 목적은 아닐 것이다. 어떻게 이 모델을 사용함으로써 이익을 추구할 수 있을까? 이 것은 상당히 중요한데 그 이유로는, 어떻게 문제화 할 것이며, 무슨 알고리즘을 선택할 것인지, 또한 모델을 평가할 때 무슨 성능 측정을 할 것인지 등에 대해 결정 할 수 있기 때문이다.

 아래의 그림(machine learning pipeline)과 같이, 주택 가격을 예측하여, 투자 할 가치가 있는지에 대해 평가 할 것이다.

 Pipelines

데이터 처리 컴포넌트의 시퀀스를 data pipeline 이라고 부른다. pipeline은 머신러닝 시스템에서 매우 일반적으로 사용되는데, 많은 데이터를 조작하고, 변환해야 되기 때문이다.

 컴포넌트는 일반적으로 비동기(asynchronously)로 실행된다. 각각의 컴포넌트들은 많은 양의 데이터를 가져와서 처리하고, 다른 데이터 스토어에 결과 값을 내보낸다. 그 후, pipeline의 다음 컴포넌트에서 이 데이터를 가져와서, 자신의 output에 내보내는 식이다. 

 각각의 컴포넌트는 독립적으로 수행되는데, 컴포넌트간의 인터페이스는 단순한 데이터 스토어다. 이를 통해, 시스템을 매우 쉽게 파악할 수 있으며, 여러 팀이 서로 다른 컴포넌트에 집중할 수 있게 된다. 

 그 다음 상사에게 물어 볼 질문으로는, 현재 솔루션(있을 시에)에 대해 묻는 것이다. 이는 문제를 해결함에 있어 통찰력을 줄 수 있다. 

 상사는 주택 가격을 측정함에 있어, 전문가에 의해 수동적으로 측정 되고 있다고 한다. 구역에 대한 최신 정보를 수집하고 있으며, 만약 주택 가격의 중간 값을 얻을 수 없을 경우, 복잡한 규칙을 사용하여 측정하고 있다고 한다. 이러한 방법은, 시간과 비용이 많이 소모되며, 측정 값도 좋지 않다. 

  이제 시스템을 설계해보자! 첫 째로, 틀을 만들어야 되는데 이것은 지도/비지도/강화학습 중 어떤 것이 적합할까? 또한, 분류, 회귀, 아니면 다른 것일까? 또한 batch 학습이 적합할까, 아니면 온라인 학습이 적합할까? 진행하기 전에 한번 생각해보는 시간을 가져보자.

 이것은 전형적인 지도학습의 문제이다. 또한, 이것은 주택 가격에 대한 측정이므로 회귀 문제가 적합하며, 더욱 정확하게는 multivariate 회귀 문제라고 할 수 있다. 그 이유는 다수의 feature를 사용하기 때문이다. 마지막으로 이 데이터는 연속적인 데이터가 들어오는 시스템이 아니기 때문에, 데이터가 급격하게 변하는 것을 조정할 필요가 없다. 그렇기 때문에 해당 시스템은 batch 학습이 적합하다.


2) Select a Performance Measure

 다음 단계로는 성능 측정(performance measure)을 선택하는 것이다. 일반적인 회귀 문제의 성능 측정으로는 평균 제곱근 편차(Root Mean Square Error, RMSE) 가 있다.

 

Notations

머신러닝에서 매우 일반적으로 사용되는 notations

  • m: 데이터(instance)의 수
  • x^(i): 데이터 셋에서 i번째의 모든 feature 값들의 벡터 (단, y^(i)값인 라벨은 제외) 
     예를 들어, 위도:33.91, 경도: -118.29, 중간 수입: 500만원 일 때, 주택 가격의 중간 값이 5억이라고 가정해보자.
     이 때, x^(1) = [33.91, -118.29, 500]^T(전치해서 벡터를 뜻함), y^(1) = 1 이 된다.
  • X: 데이터 셋의 모든 feature의 행렬
     x^1, X^2..... 들에 대해 행렬로 만든 것
  • h: 예측 함수, hypothesis라고 부름. 
     예를 들어, x^(i) 데이터의 feature 벡터가 주어졌을 때, 결과 값으로 예측 값인 y-hat^(i) =  h(x^(i))로 표현
  • RMSE(X,h): 가설 h를 이용하여 데이터를 측정하기 위한 cost 함수
    별거아닌데 작성하기가 어렵다..

 일반적으로 RMSE가 회귀 작업을 위한 성능 측정에 대해 선호하는 방법이지만, 일부 상황에 대해서는 다른 방법이 좋을 수 있다. 예를 들어, 많은 이상치 districts가 있다고 가정해보자. 이러한 경우에는, 평균 절대값 오차(Mean Absolute Error)가 이용 될 수 있다.

 RMSE와 MAE는 두 벡터 사이의 거리를 측정하는 방법이다. 

 

3) Check the Assumptions

 가설을 확인할 것. 이는 심각한 문제를 초기에 해결 할 수 있음. 예를 들어, 예측한 가격에 대해 실제 값이 아닌 "싸다, 보통이다, 비싸다"라고 카테고리화 시켜야 되는 문제였다면? 이는 회귀 작업이 아닌 분류 작업으로 바꿔야 될 필요가 있다. 다행히도, downstream system 팀원들에게 물어 본 결과, 카테고리가 아닌 실제 값이라고 한다! 다음 단계로 넘어가자!


2. Get the Data

 이 책은 jupyter notebook으로 실습을 한다. 본 포스팅에는 jupyter 설치 방법은 제외한다.


1) Download the Data

 데이터 fetch에 대해 소개한다.

데이터는 저장소로부터 불러오게끔 되어있으며, 사용하고 있는 머신에 다양성을 두기 위해 os를 import 하여 처리하고 있다. 

로컬 내 디렉토리가 없을 경우, 새로 생성된다. 다운 받는 파일이 tar 형식으로 압축되어 있기 때문에, tar파일에 대한 압축 해제 코드도 포함되어 있다.


 다음으로는 pandas를 통해, 데이터를 불러 올 수 있다. 함수를 정의해 놓음으로써, 편리하게 재 사용 할 수 있다.

위에서 tar파일을 압축 해제하면 csv파일이 생성 된다. 이 csv파일을 불러오는 것이다.

파일을 로컬 내 저장하고, 압축을 푸는 함수는 fetch_housing_data() 를 통해 호출 할 수 있고, 데이터를 pandas로 불러오는 함수는 load_housing_data() 를 통해 진행 할 수 있다.


2) Take a Quick Look at the Data Structure 

 jupyter notebook을 이용하여, 불러 온 화면은 아래와 같다.

총 20,640개의 데이터가 있으며, 10개의 컬럼, 즉 feature를 가지고 있다. 

주목해야 할 점은, ocean_proximity feature를 제외한 다른 feature들은 float64의 데이터 형식으로 되어있지만, ocean_proximity의 경우 string값으로 되어 있다는 점이다. 또한, total_bedrooms의 경우 다른 feature들은 20,640의 데이터를 가지고 있는 반면, 20,433개로 207개의 데이터가 손실이 있다. 이는 값이 들어 있지 않을 때, 처리되지 않기 때문이다. describe() 함수를 호출하면, 각각의 feature 들에 대해 개수나, 평균, 표준편차 등을 확인 할 수 있다.

 위의 ocean_proximity의 경우 string으로 되어 있다고 하였다. 이는 무엇을 의미하는 것일까? 

이것은 아마 categorical 속성이라는 것을 알 수 있다. (연속된 데이터로 이루어진 것이 아니기 때문에 regression으로 생각하지는 않을 것이다)

value_counts() 함수를 사용하여 ocean_proximity의 값들을 확인해 볼 수 있다. 5개의 값으로 구성되어 있음을 알 수 있다.

 데이터의 형태를 빠르게 확인해 볼 수 있는 히스토그램을 그려보자. 

 위 히스토그램에서 눈여겨볼 만한 사항으로는 median_income부분이 미국 달러로 표현되지 않았다는 것이다. 

또한 housing_median_age와 median_house_value 모두 상한 선을 가지고 있다는 것이다. 

마지막으로는 꼬리가 길다는 것이다. 이것은 몇몇 머신러닝 알고리즘에 있어 패턴을 인식하는 데 있어 어려움을 줄 수 있다.

추후 이러한 속성들을 종 모양의 분포(정규분포겠쥬?)를 가질 수 있도록 변형을 할 것이다.


 3) Create a Test Set 

 테스트 셋을 만드는 것은 이론적으로 매우 간단하다. 단순히 몇 개의 instance들을 랜덤으로 추출하면 된다. 일반적으로 테스트 셋은 전체 셋의 20%정도를 추출한다.

위의 방법은 잘 동작하지만, 문제가 있다. 만약 프로그램을 다시 돌린다면, 다른 테스트 셋이 생성된다.. 

이러한 문제를 해결하기 위한 방법 중 하나는 처음 돌렸을 때의 데이터를 저장하고, 이를 다시 불러오는 방법이다.

다른 방법으로는 seed 값을 설정해주는 것이다. 즉 np.ramdom.permutation을 호출하기 전에 seed값을 설정함으로써, 항상 같은 shuffled된 값들을 생성하는 방법이다.

 

 그러나 위의 두가지 방법 모두 새로운 데이터셋을 업데이트 하고 fetch 할 때 문제가 발생한다. 

이러한 문제를 해결하기 위한 방법으로는 각각의 인스턴스 식별자(identifier)를 사용하여 해당 인스턴스가 테스트 셋에 포함되어야 하는지에 대한 여부를 결정하는 것이다. 예를 들어, 각 인스턴스 식별자의 hash값을 계산한 후, hash의 마지막 값만 유지시킨다. 그 다음 51과 같거나 작을 경우 (256의 20%) 테스트 셋에 넣는다. 이러한 방식을 사용하면 데이터 셋을 새로 고치더라도 일관되게 유지 된다. 새로운 테스트 셋에는 새로운 인스턴스의 20%가 포함되겠지만, 이전 트레이닝에 사용된 셋의 어떠한 인스턴스들도 포함되지 않을 것이다.

 위의 방식으로 진행하기에는 아쉽게도 housing dataset에는 식별할 수 있는 unique한 값이 없다. 그렇기 때문에 간단한 방법으로 row index 번호를 unique한 식별자로 사용하고자 한다.

 아래의 그림을 보면 맨 왼쪽 컬럼에 index 열이 추가되었음을 확인할 수 있다. 이는 다른 행과 겹치지 않기 때문에 unique하다.

 train data와 test data로 구분하는 것은 아래와 같다.


  파이썬에서 머신러닝에 사용하는 라이브러리 중 가장 유명한 scikit-learn에도 위의 데이터를 나누는 함수를 제공한다.

가장 간단한 것으로는 train_test_split이 있다. 이것은 위의 split_train_test와 매우 유사하다. 

짧기 때문에 본문에 표시하고자 한다.

1
2
3
4
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

#https://colorscripter.com/
cs

끝이다..... 뭐 처음부터 이런방식대로 사용하는건 편리하겠지만, 약간의 이해를 하기 위해서 훑어봤다라고 생각하면 좋지 않을까싶다. 그런데 이러한 라이브러리를 사용함에 있어, 필요한 부분은 버전업이 되면서 함수들이 달라질 수 있으므로 버전과 함수명을 체크하는 것이다. (예를 들어 scikit-learn의 17버전에는 train_test_split이라는 함수를 model_selection 부분에서 찾을 수 없었지만, 19버전에서는 사용할 수 있었다.)

 전문가들이 중간 주택 가격을 예측하기 위해서는 중간 소득이 중요하다 가정해보자. 중간 소득 히스토그램을 확인해보자.

 대부분의 값들이 2에서부터 5까지 밀집되어있다. 또한 몇몇은 6을 초과하고 있다. 각 데이터 집합에 편향되지 않은 인스턴스들을 갖는 것이 중요하다.


 아래의 그림은 중간 소득에 1.5로 나눈 값을 반올림 처리하고(소득 카테고리를 제한하기 위해), 5보다 큰 값에 대해서는 5로 합치는 작업을 수행한다.

 

 다음으로는 소득 카테고리 기반으로 계층화(stratified)된 샘플링을 수행할 준비가 됐다. Scikit-learn의 StratifiedShuffleSplit 클래스를 사용할 수 있다.

 

 이제 원 상태로 돌아가기 위해 income_cat 속성을 삭제하여야 한다.



3. Discover and Visualize the Data to Gain Insights

 지금까지는 전반적인 데이터의 구조와 테스트셋의 구성에 대해 살펴보았다. 이제 우리의 목표는 조금 더 깊게 살펴보는 것이다.


1) Visualizing Geographical Data

 우리의 housing data는 경도와 위도를 가지고 있기 때문에, scatter 그래프는 좋은 아이디어로 사용 될 수 있다.

 좌측의 그래프의 경우 구분이 잘 되지 않아, 오른쪽에 alpha 값을 주었더니 시각적으로 보기 편하다. 밀집되어 있는 부분은 미국의 LA나 San Diego 뭐 이런 데 근처라고 한다. (한국의 지도도 이렇게 나오면 어딘지 잘 모르겠는데, 미국은 .. 그냥 그러려니)


 이제 주택 가격에 대해 살펴보고자 한다. 

 cmap에 미리 정의된 것들은 아래 보이는 것처럼 상당히 많다.
(가끔 이름이 생각이 안날 때, 일부러 틀리게 적고 확인하는 꼼수를... )

jet는 파랑(낮은 값)색에서 빨강(높은 값)의 범위를 가지는 color map이다.

 위의 이미지에서 볼 수 있듯이, 주택 가격은 인구밀도와, 지역과 많은 관련성(예를 들면, ocean과의 밀접)이 있다는 것을 확인할 수 있다. 이것은 클러스터링 알고리즘을 사용하여 주요 클러스터를 감지하고, 클러스터 센터의 근접성을 측정하는 새로운 feature를 추가하는 데 있어 유용하게 적용 될 수 있다.


2) Looking for Correlations

 dataset이 매우 크지 않기 때문에, corr() 함수를 사용하여 모든 속성들의 pair 사이에 표쥰 상관 계수를 쉽게 계산할 수 있다.

그리고 각 feature들이 얼마나 주택 중간 가격(median house value)과의 상관 관계가 있는지 확인할 수 있다.


 상관 계수의 범위는 -1에서부터 1까지이다. 1과 가까울 수록 강한 양의 상관관계를 표현하며, -1과 가까울수록 강한 음의 상관관계를 표현한다. 0에 가까울수록 두 데이터간의 상관관계는 적다. 아래 그림을 보면, 이해하기 쉽다.


 속성들 간의 상관 관계를 체크하기 위한 다른 방법으로는 pandas 의 scatter_matrix 함수를 사용하는 것이다. 

모든 9개의 feature들은 각각에 대해 상관 분석을 하기 때문에, 많은 양의 그래프가 출력된다. 그렇기 때문에 위의 속성 중 상위 4개에 대해서만 표현하기로 하자.

 대각선에 있는 히스토그램 그래프들을 보자. 만약 pandas가 각각의 변수를 그 자체로 plotted 하게 되면, 직선으로 가득 차게되어 유용하지 않을 것이다. 그렇기 때문에, pandas는 각각의 속성에 대해 히스토그램으로 표현하고 있다.

위에서 주택가격과 상관 관계가 가장 높았던 것은 중간 소득(median income)이었다. 이는 1행 2열의 산점도인데 좀 더 살펴보자.

 아래의 산점도를 살펴보면 두 가지를 확인해 볼 수 있다.

첫 째로, 상관 관계가 상당히 강하다. 점들이 분산되어 있지 않고, 양의 상관 관계임을 확인할 수 있다.

둘 째로, $500,000 이상의 값들은 $500,000으로 맞춰진다. 그러나 이 산점도는 $450,000, $350,000, $280,000 등에서도 수평선을 확인할 수 있다. 알고리즘이 이러한 데이터의 이상점(quirks)을 학습하고 재현하는 것을 보호하기 위해, 해당 districts를 제거할 수 있다. 


3) Experimenting with Attribute Combinations

 머신러닝 알고리즘에 대한 데이터를 실제로 진행할 때, 다양한 속성들에 대해 조합을 시도해 볼 수 있다.

예를 들어, 총 districts에 있는 방의 총 개수는, 실제로 얼마나 많은 가구들이 존재하는지 모른다면 불필요한 정보일 수 있다.

실제로 원하는 것은 가구 당 방의 갯수이다.

 마찬가지로, 총 침실 수 자체만으로는 유용한 정보를 도출할 수 없다. 이 때 필요한 것은, 방의 개수와 비교하는 것이다. 

그리고 가구당 인구 또한 흥미로운 조합의 속성일 수 있다. 다음과 같이 새로운 속성을 만들어보자.


 나쁘지 않았다. 새롭게 추가된 방 개수당 화장실 수 속성은 총 방의 개수나 침술 수보다 더 관련이 높음을 알 수 있다. 

해석해보자면 침실의 낮은 비율이 집 값이 비싼거와 연관이 있고, 가구당 방의 개수는 districts의 총 방의 개수보다 더 관련이 있다. 

4. Prepare the Data for Machine Learning Algorithms

 이제 머신러닝 알고리즘을 위해 데이터를 준비하는 시간이다. 

처음으로는 training set에 대해 깨끗한 상태로 되돌려야된다.

1
2
3
4
5
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
 
#https://colorscripter.com
 
cs

1) Data Cleaning

 대부분의 머신러닝 알고리즘은 손실된 feature값에 대해 잘 작동하지 않는다. 그렇기 때문에 이를 처리하기 위한 몇 가지 기능을 만들어야된다. 우선 2.2)에서 언급했던 total_bedrooms을 떠올려보자. 분명 20,640에서 207개가 손실된 20,433개만 가지고 있었다. 이를 수정해보자. 이를 위해 3가지 옵션이 있다.

  • Get rid of the corresponding districts.
  • Get rid of the whole attribute.
  • Set the values to some value (zero, the mean, the median, etc).

 DataFrame's의 dropna(), drop(), 그리고 fillna() 함수를 사용하면 쉽게 해결할 수 있다.

이름에서 알 수 있듯이, dropna는 nan 값들이 있는 데이터를 삭제 하는 방법

drop은 전체 속성의 값, 즉 total_bedrooms 열을 삭제, 마지막으로 fillna는 nan값들을 특정 선택하는 데이터로 채우는 것이다.

1
2
3
4
5
6
7
8
#option 1
housing.dropna(subset = ["total_bedrooms"])
#option 2
housing.drop("total_bedrooms", axis=1)    #여기에서 axis=1은 열을 뜻한다.
#option 3
housing["total_bedrooms"].fillna(median, inplace=True)    #inplace=True를 해줘야 데이터가 갱신된다.
 
#https://colorscripter.com/
cs

 

 만약 option 3을 선택한다면, training set의 중간 값을 계산해줘야 되며, training set의 빈 칸에 중간 값으로 채워줘야 된다.

Scikit-learn에서는 누락 된 값을 처리 할 수 있는 Imputer라는 편리한 클래스를 제공한다. 

먼저 Imputer 인스턴스를 생성하고, 각 속성의 누락 된 값을 해당 속성의 중앙값으로 대체하도록 지정해주면 된다.

1
2
3
4
from sklearn.preprocessing import Imputer 
imputer = Imputer(strategy ="median")
 
#https://colorscripter.com/
cs

 2.2)에서 언급했던 ocean_proximity가 어떤 특성이 있었는지 기억나는가. 그렇다. ocean_proximity는 non numeric으로 이루어져 있다. 중앙 값은 숫자 속성에서만 계산 될 수 있기 때문에 텍스트 속성 없이 데이터 사본을 만들어야 된다.

1
2
3
4
#숫자로만 이루어진 housing_num이라는 별도의 사본을 만듦
housing_num = housing.drop("ocean_proximity", axis = 1)
 
#https://colorscripter.com/
cs


 이제 fit() 함수를 사용하여 Imputer 인스턴스를 training data에 맞출 수 있다.

1
2
3
imputer.fit(housing_num)
 
#https://colorscripter.com/
cs


 Imputer는 단순히 각 속성의 중앙 값을 계산하고 그 결과를 statistics_ 인스턴스 변수에 저장한다. total_bedrooms 속성에만 값이 누락 되었었지만, 시스템이 작동 한 후에 새 데이터에 누락 된 값이 없는지에 대해 확신할 수 없기 때문에 모든 속성에 적용하는 것이 더 안전하다.

imputer.statistics_에 저장 된 값과 속성들의 중간값을 계산 한 것과 같음을 확인할 수 있다.


 

 이제 학습된 중앙값으로 누락된 값들을 대체함으로써 트레이닝 셋을 변환하기 위해 "훈련 된" imputer 를 이용할 수 있다.

결과물은 변환 된 feature 들을 포함한 Numpy 배열이다. 

 만약 pandas DataFrame으로 다시 되돌리고 싶다면 아래와 같다.


2) Handling Text and Categorical Attributes

 좀 전에 categorical 속성인 ocean_proximity를 삭제했었다. 그 이유는 텍스트로 구성 되어 있어 중간 값을 계산 할 수 없었기 때문이다. 많은 머신러닝 알고리즘은 숫자로 처리하는 것을 선호한다. 그럼 이러한 텍스트로 이루어진 것들은 사용할 수 없을까?

정답은 당연히 아니다. 이제 이 텍스트로 구성된 것을 숫자로 변환하는 작업을 해보자.

 Scikit-Learn에는 이러한 것들을 처리하기 위해 LabelEncoder 함수를 제공한다.

인코딩 된 값을 확인해보면 3,3,3....1,1,1 이라는 숫자를 확인 할 수 있다. 이는 1H OCEAN은 0, INLAND는 1... ISLAND는 4이다.


 위의 인코더의 문제점은, 머신러닝 알고리즘이 두 개의 근접한 값들이 거리가 있는 두 개의 값들보다 더 유사하다고 가정한다는 것이다. 이는 예를 들어 범주 0과 4가 범주 0과 1보다 더 유사할 수 있는 문제가 발생한다.

이러한 문제점을 해결하기 위한 방법으로 바이너리 속성을 부여하는 것이다. 이를테면 1H OCEAN이 1일 때 나머지 속성들은 0을 주는 방식이다. 이러한 방법을 one-hot encoding이라고 부른다.

즉, 1H OCEAN, INLAND, NEAR OCEAN, NEAR BAY, ISLAND 라는 5개의 각기 다른 값이 있을 때,  1H OCEAN이 1이면 10000,

NEAR OCEAN이 1이면 00100 식으로, 1은 (hot), 다른 것들은 0 (cold)가 되는 인코딩 방법이다. 하나만 hot이자나~~


 Scikit-learn은 integer categorical 값을 one-hot 벡터로 변환하는 OnehotEncoder를 제공한다. 주의해야 할 점은 fit_transform()은 2차원 배열을 기대하지만, housing_cat_encoded는 1차원 배열이라는 것을 주의해라. 그래서 우리는 재구성이 필요하다.


 결과물이 왜 numpy 배열이 아닌 sparce 행렬일까??? category가 수천 가지인 경우 매우 유용하게 적용 된다. one-hot 인코딩 후 수천 개의 열을 가진 행렬이 생성 된다. 이 행렬은 하나의 열인 1을 제외하고는 모두 0으로 채워진다. 이 0을 저장하기 위해 많은 메모리를 사용하는 것은 상당히 낭비가 아니겠는가. 그런데 이 sparce 행렬은 0이 아닌 요소의 위치만 저장한다. 
참 좋은 기능이다!


Scikit-learn에는 텍스트를 integer categories로 바꾸고 이 integer categories를 one-hot vectors로 바꿔주는 클래스 또한 제공함: LabelBinarizer


3) Custom Transformers

 Scikit-learn이 물론 유용한 transformers를 제공하고 있지만 custom 해야 할 필요가 있음.

Scikit-learn은 클래스를 생성하고 3개의 함수를 구현하는 것이 전부임
(3개의 함수란 fit(): self를 반환, transform(), 그리고 fit_transform()

 여기에서 transformer 함수는 하나의 하이퍼 파라미터로 add_bedrooms_per_room이 기본적으로  true로 설정되어 있다. 이러한 하이퍼 파라미터를 사용하면 해당 속성을 추가하여 머신러닝 알고리즘에 도움이 되는지에 대한 여부를 쉽게 확인할 수 있다.


4) Feature Scaling

 데이터를 적용하기 위해 가장 중요한 변형 방법 중 하나는 feature scaling이다. 특이한 경우를 제외하고는 대부분의 머신러닝 알고리즘은 매우 다른 scale에 대해 정상적으로 동작하지 않는다. housing data에 대해 상이한 scale을 확인해보자.

 total_rooms의 경우, 6에서부터 39,320 까지의 범위를 가진다. 이에 반해, median_income의 경우 0.5에서부터 15의 범위를 가진다.


 이러한 문제를 해결하기 위한 방법으로는 min-max scaling과 standardization이 있다.

  • Min-max scaling(많은 사람들은 정규화(normarlization)라고도 부른다)
    - 0에서부터 1까지의 범위로 만드는 방법으로 간단하다. 주어진 값에서 최소값을 빼주고, 최대값에서 최소값을 뺀 값을 나눠주면 된다.

    Scikit-learn에서는 MinMaxScaler를 제공한다. 한가지 재밌는 사항으로는 0-1의 범위를 원하지 않을 때, feature_range라는 하이퍼 파라미터를 제공해주기 때문에 범위를 설정해줄 수 있다.

  • Standardization
    - 위의 정규화 방식과 꽤 다르다. 먼저 평균 값을 빼주고(그렇기 떄문에 standardization값은 항상 평균값이 0이다), 분산(variance)로 나눠준다. 정규화와 달리 standardization은 특정 범위로 한정되지 않는다. 이 점은 몇몇 알고리즘에 문제가 될 수 있는데 그 중 하나는 neural network의 경우 input 값이 0에서부터 1까지의 값을 집어넣기 때문에 적합하지 않을 수 있다.
    하지만 장점으로는 이상점(outlier)에 덜 영향을 받는다는 것이다. 예를 들어, district의 중간 소득 값을 실수로 100이라고 가정해보자. Min-max scaling의 경우, 0-15의 범위로부터 0-0.15로 모든 다른 값들을 망칠 수 있다.
    Scikit-learn에서는 StandardScaler를 제공한다.

5) Transformation Pipelines

 데이터 변환에 있어, 많은 일련의 순서로 실행된다는 것을 확인해 볼 수 있었다. Scikit-learn에서는 Pipeline 클래스를 제공함으로써 이러한 변환에 있어 도움을 준다. 

 Pipeline 생성자는 이름과 추정자(estimator)가 쌍(pair)인 리스트로 구성되어 있다. 마지막 추정자를 제외하고는 반드시 transformers 를 가져야 한다. (즉, fit_transform() 함수를 가져야만 한다.)

 이름은 원하는 대로 지정이 가능하다. (두개의 밑줄 "__"을 포함하지 않는다면)

 pipeline's fit() 함수를 호출하면, 각 호출의 출력 값을 매개 변수로 전달하여 최종 추정량에 도달할 때까지 fit_transform() 함수를 순차적으로 호출하게 된다. 

 pipeline은 최종 추정자로써 동일한 함수를 제공한다. 예를 들어 마지막 추정자는 transformer인 StandardScaler다. 그렇기 때문에 pipeline은 모든 데이터 변환에 순차적으로 적용하는 transform() 함수를 가진다. (또한, fit()과 함께 transform()을 호출하는 대신에 사용했던 fit_transform() 함수를 가진다) 

 

 이제 숫자 열(numerical columns)을 numpy 배열에 수동적으로 추출하는 대신, pandas 데이터 프레임을 이용하여 직접 pipeline에 넣는다면 좋을 것이다. Scikit-learn에는 pandas 데이터 프레임을 다루는 방법은 없지만, 우리는 이 작업을 위해 custom transformer를 만들 수 있다.

 우리의 DataFrameSelector 함수는 원하는 속성을 선택하고, 나머지 부분은 삭제하며, Dataframe을 numpy 배열로 변환한다.

이를 통해, 우리는 쉽게 pandas 데이터 프레임을 이용하여 numerical values 만을 다룰 수 있는 pipeline을 작성할 수 있다.

pipeline은 숫자 속성을 가진 것만 선택할 수 있는 DataFrameSelecor로 시작하고, 이전에 논의한 전처리 단계를 거쳐야 된다. 또한 DataFrameSelecor를 사용하여 categorical 속성을 선택한 다음 LabelBinarizer을 적용하여 Categorical 속성에 대한 다른 pipeline을 쉽게 작성할 수 있다.

 이 두개의 pipeline을 하나로 합칠 수는 없을까?

당연히 존재한다. Scikit-learn에는 FeatureUnion 클래스를 제공한다. 리스트 형식의 transformer를 보내면 transform() 함수가 호출된다. 이 때 각각의 transformer가 병렬적으로 실행되며, 결과 값을 기다린다. 그 다음 각각의 transformer가 fit() 함수를 호출하면 최종 fit() 함수가 호출되어 결과를 합친다.


5. Select and Train a Model

 아... 여기까지는 잘됐다. 근데 모델 학습을 위한 housing_prepared에서 에러가 난다.

( TypeError: fit_transform() take 2 positional arguments but 3 were given)


 이 부분은 좀 더 확인이 필요할 듯.... 뒷부분은 학습데이터가 없기 때문에 진행 못함

상당히 찜찜해서 빠른 시일내에 확인해서 업데이트 예정 (9월 4일)


 해결했다. https://github.com/ageron/handson-ml/issues/75 이슈사항에 이미 등록되어 있었다.

아래 LabelBinarizerPipelineFriendly 클래스를 생성해준다.

다음으로는 cat_pipeline쪽에 기존의 LabelBinarizerPipeline부분을 LabelBinarizerPipelineFriendly로 대체해준다.


 이제 정상적으로 데이터를 뽑아보자. 이 화면을 그렇게 보고싶었다.........


1) Training and Evaluating on the Training Set

 이제 원없이 모델을 돌려보자. 첫 번째로는 선형 회귀 모델을 사용하여 학습시켜보자.

  예상치가 정확하지는 않지만(예를 들어, 첫 예측인 40%정도밖에 안됨) 작동은 된다. 

이제 평균제곱근오차(Root Mean Square Error, RMSE)를 측정하기 위해 Scikit-learn의 mean_squared_error 함수를 사용해보자.

 아무것도 안한 것보다는 좀 나아졌다. 그러나 좋은 점수는 아니다. 대부분의 districts의 중간 주택 가격이 $120,000에서 $265,000 이기 때문에 예측 에러가 $68,628이라는 숫자는 만족스럽지 못하다. 이러한 예는 학습 데이터의 underfitting 문제다. 

이러한 underfitting 문제가 발생했다는 것은 좋은 예측을 위한 충분한 정보가 제공되지 않거나, 모델이 강력하지 않다는 것을 의미한다. 이러한 underfitting을 해결하기 위한 방법으로는 보다 강력한 모델을 선택하거나, 더 좋은 feature들로 학습 알고리즘을 사용하거나, 모델의 제약을 줄이는 것이다. 

 이 모델은 정규화(regularized)되지 않았기 때문에 모델의 제약을 줄이는 것은 제외된다. 
(본문에는 없지만, 간단하게 얘기하면 정규화 즉 규제를 할 때, 알파값을 설정하여 모델의 규제를 완화시키거나 강화시킴으로써 언더피팅과 오버피팅 문제를 해결 하는 방법이 있음)

 

 더 많은 기능을 추가 할 수 있지만 우선 더 복잡한 모델을 사용해보자.

이번에는 의사 결정 ㄴ 회귀(Decision Tree Regressor)로 학습시켜보자. 이것은 강력한 모델로, 데이터에서 복잡한 비선형 관계를 찾을 수 있는 모델이다. (6장에서 더 자세히 살펴 볼 것이다.)

 0이란다. 딱봐도 이건 잘못되었다는 것을 알 수 있을것이다. 이러한 것을 지나치게 과적합되었다고해서 overfitting 이라 한다.

어떻게 확신할 수 있을까? 앞에서 살펴봤듯이, 확신 할 수 있는 모델이 생성되기 전까지 테스트 데이터를 건드리는 것은 원하지 않을 것이다. 이 때, 학습용 데이터의 일부를 훈련에 사용하고, 그 중 일부를 모델 검증용으로 사용하게 된다. 

즉, 학습용+테스트용 -> 학습용+검증용+테스트용 으로 된다는 것이다. 물론 학습용=학습용일부+검증용 이다.


2) Better Evaluation Using Cross-Validation

 의사 결정 나무 모델을 평가하는 방법 중 하나는, train_test_split 함수를 사용하여, 학습용 셋을 학습용 셋과 검증용 셋으로 분할 한 후, 분할 된 학습용 셋으로 모델을 학습시키고, 검증용 셋으로 모델을 평가하는 방법이 있다.

즉, train_test_split 함수 사용 -> 학습용셋을 학습용과 검증용으로 분할 -> 학습용으로 모델 학습 -> 검증용으로 모델 평가

위의 방법은 어렵지 않고 상당히 잘 작동한다.


 훌륭한 대안으로는 Scikit-learn의 cross-validation feature를 사용하는 것이다. 아래는 K-fold cross-validation 을 수행하는 그림이다. 이것은 무작위로 fold라고 불리는 10개의 subset으로 학습 데이터 셋을 분리한다. 그 다음 의사결정나무(decision tree) 모델로 10번 훈련시키고 평가 한다. 매 번 평가를 위해 다른 fold를 골라내고 또 다른 9개의 fold를 훈련시킨다.

그 결과는 10개의 평가점수를 포함한 배열로 표현된다.

이제 결정트리가 그 전 만큼 좋아 보이지는 않는다. 실제로 선형 회귀 모델보다 안좋아보인다. 교차 검증을 하면 모델의 성능 측정 뿐만 아니라 추정치의 정확성(즉, 표준편차) 또한 측정 할 수 있다는 것을 기억하자. 결정 트리는 +-2,728의 편차를 갖는, 약 71,265의 점수를 얻었다. 

 하나의 검증 셋만을 사용한다면, 이런 정보를 얻을 수 없다. 하지만 교차 검증은 여러 번 모델을 훈련시키는 비용이 발생하기 때문에 항상 가능한 것은 아니다.

  똑같은 방식으로 선형 회귀 모델에도 적용해보자.


 다음으로는 랜덤포레스트(Random Forest)를 실행해보자. 랜덤포레스트는 기본적으로 조금씩 다른 여러 결정 트리의 묶음이다. 랜덤 포레스트의 아이디어는 서로 다른 방향으로 과대적합된 트리를 많이 만들어내면 그 결과를 평균냄으로써 과대적합된 양을 줄일 수 있다는 것이다. 이렇게 하면 트리 모델의 예측 성능은 유지되면서 과대적합이 줄어드는 것이 수학적으로 증명 되었다.

랜덤 포레스트는 7장에서 더욱 자세하게 살펴 볼 것이다. 

 훨씬 더 좋은 결과를 얻었다. 그래도 아직 overfitting 되어 있다. 여기까지만 일단 진행하자.


Tip

파이썬의 pickle 모듈이나, sklearn.externals.joblib을 통해, Scikit-learn 모델을 쉽게 저장할 수 있다.


1
2
3
4
5
6
7
from sklearn.externals import joblib
 
joblib.dump(my_model, "my_model.pkl")
#나중에 불러 올 때
my_model_loaded = joblib.load("my_model.pkl"
 
#https://colorscripter.com/
cs


6. Fine-Tune Your Model

추후 진행 예정


댓글을 달아 주세요

블로그 이미지

꽃경남

카테고리

분류 전체보기 (127)
프로그래밍 (31)
기타 (48)
해킹 (48)