사이킷런 찍먹시리즈(3) - 손글씨 분류 모델

16 분 소요

서론

본 글은 작성자의 공부를 위해 쓰는 게시물입니다.

아래 작성하는 글은 틀린내용이 있을수도 있으니 참고하시기 바라며, 책은 핸즈온 머신러닝 2판을 사용하여 공부하고 있습니다.

손글씨 분류 모델

이번 게시물은 mnist 데이터셋을 불러와

숫자를 쓴 손글씨의 모양을 보고 이 손글씨가 어떤 숫자를 나타내는지 예측해보는 모델을 Classification 모델을 통해 만들어보도록 할 것이다.

먼저 sklearn 데이터셋에서 데이터를 불러오자.

1
2
3
4
5
6
from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version = 1, as_frame = False)
mnist.keys()

dict_keys(['data', 'target', 'frame', 'categories', 'feature_names', 'target_names', 'DESCR', 'details', 'url'])

as_frame 설정을 False로 해주면 기본값인 판다스 데이터프레임으로 파일을 받아오게 된다.

이 데이터를 살펴보면, data는 28*28픽셀들과 각 픽셀의 농도가 0-255로 나타내져 있고,

target은 해당 데이터가 어떤 글씨를 쓴것인지에 대한 정보, 우리가 예측해야할 정보가 담겨있다.

1
X, y = mnist['data'], mnist['target']

각 데이터와 타겟을 X, y에 넣고, 이제 data가 의미하는 사진을 그려보도록 하자.

1
2
3
4
5
6
7
8
import matplotlib as mpl
import matplotlib.pyplot as plt
import pandas as pd

some_digit = X[0]
some_digit_image = some_digit.reshape(28,28)

plt.imshow(some_digit_image, cmap = 'binary')

some_digit에 data의 첫번째 열에 해당하는 픽셀값을 넣어주고, 지금은 28*28의 형태가 아니라 784의 1차원 행렬로 나타내져 있으므로

reshape 메서드를 통해 784의 1차원 행렬을 28*28의 정방형 행렬로 바꾸어준다.

그 후 matplotlib의 imshow를 통해 글씨를 출력해준다.

컬러맵은 ‘binary’로 하여 흑백이 되도록 한다.

image1

이렇게 글씨가 잘 출력되는것을 볼 수 있는데,

손글씨이다 보니 5인지 s인지 헷갈린다.

정답이 target에 있으니 y의 첫번째 값을 확인해보면,

1
print(y[0])

‘5’

5를 쓴 글씨임을 알아 낼 수 있다.

지금 숫자에 따옴표가 있는걸로 봐서 traget의 type이 str, 문자열로 되어있는데, 머신러닝 알고리즘은 대부분 숫자을 원하므로 type을 숫자로 바꿔준다.

1
y = y.astype(int)

이제 데이터를 들여다 보기 전에 트레인 셋과 테스트 셋으로 먼저 나누어서 이 모델과 나 둘다 테스트셋을 들여다 보지못하도록 하자.

사실 이 데이터는 이미 섞인 채로 60000번까지가 트레인셋, 그 이후부터 끝까지가 테스트셋으로 이미 나누어져 있기때문에 우리는 그냥 변수대입만 해주어도 된다.

1
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

그렇다면 우선 모든 데이터를 분류해보기 전에, 간단하게 위에서 보았던 5라는 숫자를 쓴글씨인지 아닌지만을 분류하는 이진분류기를 먼저 만들어보자.

​우리가 만들 모델은 이 숫자가 5인지 아닌지만을 판별하는 것이므로, 타겟이 5인 데이터만 타겟데이터로 따로 분류해줄 필요가있다.

1
2
y_train_5 = (y_train == 5)
y_test_5 = (y_train == 5)

이렇게 모은뒤, 분류를 위해 사이킷런에서 머신러닝 모델을 하나 가져오겠다.

이번에는 SGDClassfier를 사용할건데, SGD는 Stochastic Gradient Decsent라는 뜻으로

확률적 경사하강법을 사용한 분류 알고리즘이다.

선형회귀에서 배웠던 경사하강법을 랜덤으로 추출하여 진행하는 방식으로, 기존 경사하강법에 비해 학습 중간 과정에서 진폭이 크고 속도가 매우 빠르다.

또 이 방법은 한번에 하나씩 훈련샘플을 독립적으로 처리하기 때문에
큰 데이터셋을 효율적으로 처리한다.

​그럼 SGDClassfier를 불러온뒤 X_train과 y_train을 학습시켜주어보자.

1
2
3
4
5
6
from sklearn.linear_model import SGDClassifier

sgd = SGDClassifier(random_state=13)
sgd.fit(X_train, y_train_5)

sgd.predict([some_digit])

​——
​Out : array([ True])

​5의 손글씨였던 some_digit의 값이 True, 즉 이 손글씨가 나타내는 수가 5라고 이 모델이 예측한것을 볼 수 있다.

그럼 이제 한개의 데이터가 아니라 모든 데이터에 대해서 이 손글씨가 나타내는 숫자가 5인지 아닌지 분류하는 이 모델의 성능을 평가해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, shuffle=True, random_state=41)

for train_index, test_index in skfolds.split(X_train, y_train_5):
    X_train_fold = X_train[train_index]
    y_train_fold = y_train_5[train_index]
    X_test_fold = X_train[test_index]
    y_test_fold = y_train_5[test_index]
    clone_sgd = clone(sgd)
    
    clone_sgd.fit(X_train_fold, y_train_fold)
    y_pred = clone_sgd.predict(X_test_fold)
    correct = sum(y_pred == y_test_fold)
    print(correct/len(y_pred))

교차 검증은 모델을 평가하는 아주 좋은 모델이기 때문에

교차검증으로 이 모델을 평가해보도록 하겠다.

단, cross_val_score() 함수를 이용할 수도 있지만, 우리가 교차검증을 하다보면 더 세부적인 교차 검증 과정을 제어해야할때가 있다.

따라서 이번에는 cross_val_score() 함수와 동일한 일을 하지만, 더 세부적인 조정이 가능하도록 코딩해보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, shuffle=True, random_state=41)

for train_index, test_index in skfolds.split(X_train, y_train_5):
    X_train_fold = X_train[train_index]
    y_train_fold = y_train_5[train_index]
    X_test_fold = X_train[test_index]
    y_test_fold = y_train_5[test_index]
    clone_sgd = clone(sgd)
    
    clone_sgd.fit(X_train_fold, y_train_fold)
    y_pred = clone_sgd.predict(X_test_fold)
    correct = sum(y_pred == y_test_fold)
    print(correct/len(y_pred))

statifiedKFold 메서드는 n_split로 폴드의 개수를 정하는 메서드인데, .split으로 인덱스값을 반환한뒤,그 인덱스 값으로 각 데이터셋을 3등분 하여 _fold 변수에 넣어준다.

이렇게 하면 교차검증을 위한 데이터셋이 완성된다.

​우리의 최종목표는 모든 숫자의 글씨를 분류하는 것이고, 지금은 먼저 5가 맞는지 아닌지를 판별하는 분류기를 만드는 단계이므로 데이터를 복사하여 5판별기가 잘작동하는지를 우선적으로 보도록 하자.

​0.95735

0.96675

0.97035

모든 교차검증에 대한 정확도가 95퍼센트 이상의 확률을 보였다.

이 모델은 SGDclassfier 한번으로 이렇게 높은 확률의 모델이 된것인데, 이 모델을 실제로 적용해도 과연 높은 확률로 예측이 가능할까?

​여기서 우리는 이 확률의 오류를 눈치채야한다.


​```python
​from sklearn.base import BaseEstimator

class neverfive(BaseEstimator):
def fit(self, X, y= None):
return self
def predict(self, X):
X = np.zeros((len(x),1), dtype = bool)
​```

BaseEstimatorf를 사용하여, 모든 데이터의 판별값을 0, 즉 False로 반환하는 변환기를 만들어준다.

이 변환기를 사용하여 교차검증을 진행하면,

1
2
3
from sklearn.model_selection import cross_val_score

cross_val_score(neverfive, X_train, y_train_5,cv = 3, scoring = 'accuracy')

array([0.91125, 0.90855, 0.90915])

모든 데이터가 5가 아니라고 했는데, 교차검증 세번 모두 90퍼센트 이상의 정확도가 나왔다.

이 데이터에서 5가 차지하는 비율이 10퍼센트가 안되기때문에, 모든 데이터를 5가 아니라고 판단해도 정확도가 90퍼센트 이상 나오게 되는것이다.

이러한 예를 통해 분류에서는 정확도를 성능평가 지표로 사용하기에는 어려운 점이 있음을 알 수 있다.

특히 어떤 클래스의 데이터가 다른 데이터보다 월등히 많은경우는 더더욱
정확도를 성능평가 지표로 사용하기에는 어려울 것이다.

따라서 분류기의 성능을 평가하는 더 좋은 방법이 필요한데, 바로 오차행렬을 사용하는 것이다.

​오차행렬의 기본적인 아이디어는, 클래스 A,B가 있을 때 클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는것인데, 예를들어 분류기가 숫자 5를 5가아닌 3으로 잘못분류한 횟수를 알고싶다면 우리는 오차 행렬의 5행 3열을 보면된다.

​이러한 오차 행렬을 만들기 위해선 실제 타겟값과 비교할 수 있는 예측값을 만들어야 한다.

이러한 예측값은 cross_val_predict() 메서드를 사용한다.

이 메서드는 평가점수를 반환하는 cross_val_score()와 달리 예측값을 반환한다.

1
2
3
from sklearn.model_selection import cross_val_predict

y_train_pred = cross_val_predict(sgd, X_train, y_train_5, cv = 3)

이렇게 하면 y_train_pred는 교차검증하여 예측한 값들이 된다.

이러한 예측값과 실제값 사이의 오차 행렬을 만들면,

1
2
3
from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_5, y_train_pred)

array([[53570, 1009], [ 1301, 4120]])

이러한 오차 행렬을 얻을 수 있는데, (1,1) 값은 진짜 음성(TrueNegative),

(1,2)는 거짓음성 (FalseNegative),

(2,1)은 가짜 양성 (FalsePositive),

(2,2)는 진짜 양성 (True Positive)라 한다.

​헷갈릴수가 있는 개념인데, 표로 참고하면 더욱 직관적으로 알 수 있다.

image1

​행은 실제 데이터의 분류값이고, 열은 예측한 데이터의 분류값이다.

​위 오차행렬을 바탕으로 이 모델을 분석해보면 53570개의 5가아닌 데이터를 5가아니라고 분류 했고 1009개의 5가아닌 데이터를 5라고 잘못 분류했다.

또, 1031개의 5인 데이터를 5가 아니라고 잘못 분류 했으며, 4120개의 5인 데이터를 5라고 분류했다.

​이러한 데이터를 바탕으로 정밀도와 재현율을 측정해 볼텐데,

​먼저 정밀도란,

진짜 양성(TP) / (진짜양성(TP) + 거짓양성 (FT))

​의 값으로, 양성데이터중에서 제대로 양성판별한 데이터의 비율을 구한것이다.

정밀도는 양성샘플만 측정해도 얻을 수 있는 값이지만, 다른 모든 양성샘플을 무시하기 때문에 그리 유용하지 않다.

따라서 재현율과 함께 사용하게 되는데,

재현율은

진짜 양성(TP) / (진짜양성(TP) +거짓음성(FN))

​의 값으로, 분류기가 정확하게 감지한 양성샘플의 비율을 나타낸다.

헷갈리지만 간단하게 요약하면, 정밀도는 예측데이터가 양성인 데이터의 정답률에 대한 값이고, 재현율은 실제 데이터가 양성인 샘플인 데이터에서 정답률에 대한 값이다.

​재현율과 정밀도는 서로 트레이드 오프 관계에 있는 값인데, 정밀도가 증가하면 재현율을 감소하고 재현율이 증가하면 정밀도는 감소한다.

​이 재현율과 정밀도의 값을 보통 하나의 값으로 나타내면 편한데, 이를 F1점수라 한다. F1점수는 재현율과 정밀도의 조화평균으로, F1 점수는 재현율과 정밀도가 비슷할수록 올라간다.

​하지만 F1점수가 높다고 무조건 좋은것이 아니다. 우리는 분류기 모델이 어떤 상황에 쓰이는지에 따라 재현율과 정밀도의 값을 조절해야 하는데, 예를 들어 유해 컨텐츠를 걸러내는 분류기를 제작한다고 하면 좋은 동영상들이 몇개 잘못 걸러지더라도 나쁜 영상만을 확실히 걸러주는것이 더 바람직한 방향이기 때문에 이러한 모델에는 정밀도가 높은 모델이 더 적합할 것이다.

​또 반대로 우리가 cctv로 도둑을 발견하면 경비원을 호출하는 시스템을 만든다고 생각해보자. 도둑은 한번이라도 들면 큰 피해를 입기 때문에, 가끔 오류가 나더라도 모든 도둑을 잡아내는 것이 중요하다.

따라서 이 모델에서는 가끔 오류로 인해 경비원이 호출되기는 하겠지만 재현율이 높은 것이 더 적합 할것이다.

​이처럼 재현율과 정밀도는 상황에 따라 어느것을 더 높게 설정할지 선택해야 한다. 이를 정밀도/재현율 트레이드오프 라고 한다.

​이러한 정밀도와 재현율는 우리가 임곗값을 어떻게 정하느냐에 따라 달라진다.

임곗값은 한마디로 분류기준 점수를 몇점으로 나눌것인지에 대한 결정점수의 기준값인데, 임곗값이 높아질수록 정밀도가 높아지고, 임곗값이 낮아질수록 정밀도가 낮아진다.

임곗값을 구하기 위해 모든 데이터의 결정함수의 점수를 알아내야 하기 때문에, 이번에도 역시 cross_val_predict를 사용해 보도록 한다.

1
y_scores = cross_val_predict(sgd,X_train, y_train_5, cv = 3, method = 'decision_function')

이 결정함수값을 precision_recall_curve를 이용하여 예측값과 재현율, 임곗값을 알아보도록하는데, 숫자만 보면 그리 직관적이지 못하므로, matplotlib를 사용하여 시각화 해보자.

​```python
​from sklearn.metrics import precision_recall_curve

precision, recall, threshold = precision_recall_curve(y_train_5, y_scores)

plt.plot(threshold, recall[:-1])
plt.plot(threshold,precision[:-1])
​```

​​image1
​​
​​x축은 임곗값, 파란선은 재현율, 주황선은 정밀도를 나타내는 그래프를 얻을 수 있는데, 임곗값에 따라 재현율과 정밀도가 서로 변화하는것을 확인할수가 있다.

또, 좋은 정밀도/재현율 트레이드오프를 결정하는 다른 방법은, 재현율에 따른 정밀도의 그래프를 그려보는 것이다.

1
plt.plot(recall, precision)

​​image1
​​
​​x축은 재현율, y축은 정밀도인데, 특정 구간에서 부터 재현율에 따른 정밀도가 급격하게 감소하는걸 볼 수 있다.

이 그래프에서는 재현율이 0.8을 넘어가면 정밀도가 급격히 감소하는데, 이 하강점 직전을 정밀도/재현율 트레이드 오프로 사용하는 것이 좋다.

예를들어 0.6정도의 지점정도를 사용 할 수 있을것이다.

​이러한 그래프는 우리가 정밀도와 재현율의 값을 정할 때 아주 유용한데, 예를들어 정밀도 90퍼센트의 모델이 필요하다면 위 임곗값, 정밀도,재현율 그래프에서 정밀도 90퍼센트에 해당하는 임곗값을 설정하면 될것이다.

대략 8000정도의 임곗값을 가질 것으로 예상되는데, 정확한 값을 얻기 위해서는 np.argmax() 메서드를 사용한다.

1
2
3
4
5
6
7
threshold_90 = threshold[np.argmax(precision >= 0.90)]

y_train_pred_90 = (y_scores > threshold_90)

precision_score(y_train_5, threshold_90)
recall_score(y_train_5, y_train_pred_90)


0.9000263782643102

0.6294041689725143

​np.argmax() 메서드는 최댓값의 인덱스 값을 반환하게 되고, 정밀도가 90퍼센트 이상인 최댓값(True)의 인덱스를 반환하게 된다.

따라서 정밀도와 재현율의 값을 보면, 우리가 정밀도 90퍼센트, 재현율 63퍼센트의 분류기를 만든 것을 확인 할 수 있다.

이번엔 또 다른 도구로 수신기 조작 특성 (ROC) 곡선에 대해 알아보자.

ROC 곡선은 거짓 양성 비율 (FPR) 에 대한 진짜 양성 비율 (TPR)의 곡선으로, 여기서 거짓 양성 비율은 양성으로 잘못 분류된 음성 샘플(FPR), 1에서 진짜 음성 비율 (TNR)을 뺀값이다.

진짜 음성 비율 (TNR)은 특이도라고도 한다.

그러므로 ROC곡선은 재현율에대한 (1 - 특이도)의 그래프이다.

​​image1

ROC 곡선은 roc_curve 메서드를 통해 FPR과 TPR을 받아와서 그릴 수 있다.

1
2
3
4
fpr, tpr, threshhold = roc_curve(y_train_5, y_scores)

plt.plot(fpr,  tpr, color = 'b') 
plt.plot([1,0],[1,0],'k--' )

​​image1
​​
그림과 같은 ROC 곡선을 얻을 수 있는데, x 축은 거짓양성 비율이고, y축은 진짜 양성비율 (재현율)이다.

이 그래프에서 곡선 아래의 면적, AUC를 구하면 분류기들의 성능을 비교할 수있다.

검은색 점선이 AUC가 0인 완전히 랜덤한 분류기로, 아래의 면적이 0.5를 가지며, 곡선이 완전한 직각을 그려 AUC가 1이되면 완벽한 분류기이다.

​roc_auc_socre() 메서드를 통해 이 ROC 곡선의 AUC를 측정할 수 있다.

1
2
3
4
from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_5, y_scores)


0.9589703559022139

​대략 0.96의 값을 가진다.

보통 양성클래스가 드물거나, 거짓음성보다 거짓양성이 더 중요할때 PR곡선을 사용하고, 그러지 않은 경우에는 ROC 곡선을 보통 사용하는데,

예를들어 어떤 주식시장에서의 이상 거래를 검출하는 모델이라고 하면 정상거래가 대부분이고 이상 거래는 아주 적은 1퍼센트가량일것이므로 PR곡선을 사용하고, 어떠한 병을 진단하는데 쓴다고 할때는 검사자들이 이미 어느정도의 이상징후를 느낀 사람만을 대상으로 병이 있는지를 진단할 것 이기 때문에 병이 있을 확률이 상대적으로 높으므로 ROC곡선을 사용한다.

물론 희귀병이라던가, 상황별로 분석 방법이 조금 달라질 수 있다는것을 염두에 두러야 한다.

​한번 다른 ROC 곡선을 그려 SGDClassfier의 ROC 곡선과 한번 비교해보도록 하자.

RandomForest 모델을 사용하여 학습시켜 볼건데, 앞에서와 마찬가지로 동일하게 모델을 불러와 학습시켜볼 것이다.

1
2
3
4
5
from sklearn.ensemble import RandomForestClassifier

rdforest = RandomForestClassifier()

rdforest.fit(X_train, y_train_5)

위와 동일하게 교차검증을 통해 결정함수의 값들을 받아와야 하는데, RandomForest 모델에는 결정함수를 불러와주는 메서드인 decision_funcion() 메서드가 없다.

대신 predict_proba() 메서드가 있는데, 사이킷런 분류기들은 일반적으로 이 두 메서드중 하나를 갖거나 두가지 모두를 갖고있다. predict_proba() 메서드는 예측 확률을 반환하는 메서드로 샘플이 행, 클래스가 열이고 샘플이 주어진 클래스에 속할 확률을 담은 배열을 반환한다.

  음성클래스 양성클래스
클래스에 속할 확률 확률 1 확률 2
1
y_probas_forst = cross_val_predict(rdforest,X_train, y_train_5, cv = 3, method = 'predict_proba')

이렇게 반환받은 확률값중에, 배열의 두번째 값인 양성클래스에 속할 확률을 사용하여 ROC곡선을 그려보도록 할 것이다.

SGDClassfier와의 비교를 위해 아까 그린 ROC 곡선과 동일한 플롯에 그려보도록 하자.

1
2
3
4
5
6
7
from sklearn.metrics import precision_recall_curve
y_scores_forest = y_probas_forst[:,1]
fpr_f, tpr_f, threshold_f = roc_curve(y_train_5, y_scores_forest)

plt.plot(fpr,tpr, 'b:')
plt.plot(fpr_f, tpr_f, color = 'r', label = 'SGD')
plt.plot([1,0],[1,0],'k--', label = "random")

​​image1

아까의 ROC 곡선은 파란 점선으로 바꿔 그리고, RandomForest의 ROC 곡선은 빨간 실선으로 나타냈다.

RandomForest의 AUC는 거의 1에 가까운걸 볼 수 있고, 이 ROC 곡선을 통해 RandomForest 모델이 SGDClassfier 모델보다 성능이 뛰어나다는 것을 알 수 있다.

​이 ROC 곡선의 AUC값을 구해보면,

1
2
3
from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_5, y_scores_forest)

0.9984074437890377

거의 99퍼센트에 가까운 좋은 성능을 갖고있음을 알 수 있다.

​그럼 이제 5와 5가아님을 분류하는 이진 분류기를 만들고, 그 성능을 높였으니 이제 모든 숫자를 분류해내는 다중 분류기(또는 다항분류기)를 만들 차례다.

일부 알고리즘들(SGDClassfier, RandomForestClassfier, NaiveBayesClassfier)은 여러개의 클래스를 직접 처리할 수 있지만, 몇몇 클래스(LogisticRegression, SupportVectorClassfier)는

이진 분류만 가능하다. 하지만 이러한 이진 분류기를 여러 개 사용하여 다중클래스를 분류하도록 만들 수 있다.

​예를 들어, 0에서 9까지를 분류하는 경우에는 10개의 이진분류기를 만들어 동시에 작동시키고 가장 점수가 높은 분류기의 클래스를 선택하면 된다.

이를 OvR (One-Versus-the-Rest) 전략이라고 한다. (또는 OvA (One-Versus-All) 라고도 함)

또 다른 전략은 0과 1구별, 0과 2구별, 1과 2구별 이런식으로 각 숫자의 모든 조합의 이진 분류기를 훈련시키는 방법이다. 이를 OvO(One-Versus-One) 전략이라고 하는데, 분류해야 할 클래스의 개수가 N개라면 분류기는 N * (N-1)/2개가 필요하다.

위의 글씨분류 데이터에서는 클래스가 10개이므로 10(10-1)/2 = 45 개의 분류기를 훈련시켜야 한다.

이미지 하나를 분류할때마다 45개의 분류기를 모두 통과시켜 가장 양성으로 많이 분류된 클래스를 선택하게 된다.

OvO 전략은 각 분류기의 훈련에 전체 데이터가 아니라 일부데이터, 예를들어 0과 1분류기라면 0의 샘플데이터와 1의 샘플데이터만이 필요하기 때문에 훈련 세트 크기에 민감하여 작은 훈련세트로 여러 분류기를 학습시키는것이 빠른 모델의 경우 OvO전략이 선호되고, 그렇지 않은 대부분의 알고리즘은 OvR 전략을 선호한다.

​다중 클래스 분류 작업에 분류 알고리즘을 선택하면 사이킷런이 알아서 OvO 혹은 OvR을 자동으로 실행해준다.

​SupportVectorClassfier를 사용해 다중 분류를 해보도록 하자.

1
2
3
4
5
from sklearn.svm import SVC

svc = SVC()

svc.fit(X_train, y_train)

트레이닝 셋을 학습 시켰으니, 앞에서 분류기를 확인하기 위해 사용했던

5의 글씨인 some_digit 샘플을 넣어 예측해보자.

1
2
3
svc.predict([some_digit])

array([5])

이 글씨가 5라고 알맞게 예측하였다.

이 다중분류기가 어떠한 방식을 통해 5라고 예측하였는지를 알아보기 위해 decision_function을 사용하여 결정함수를 출력 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
svc.decision_function([some_digit])

array([[ 1.72501977,  2.72809088,  7.2510018 ,  8.3076379 , -0.31087254,
         9.3132482 ,  1.70975103,  2.76765202,  6.23049537,  4.84771048]])
```

10개의 값을 가진 배열이 나왔는데,  숫자들은 각각 1부터 10까지의 점수이다.

5 예측에 대한 점수가 9.31 가장 높은데,  값을 바탕으로  분류기는  데이터가 5라는것을 예측했다는 것을   있다.

이와 동일하게 SGDClassfier를 사용하여 다중 분류를 해보자.

```python
sgd.fit(X_train, y_train)

sgd.predict([some_digit])

array([5])

마찬가지로 some_digit를 5라고 잘 분류한것을 알 수 있다.

SGDClassfier의 결정함수도 한번 살펴보자.

1
2
3
4
sgd.decision_function([some_digit])

array([[-13681.24920981, -28389.42999311,  -9577.30296028, 1976.11619205, -35783.52617472,  
         2419.27374993, -23922.00957152, -13108.23496675, -10340.0266054 , -13631.60886894]])

역시 SGDClassfier도 클래스 5에 해당하는 점수가 대략 2419로 가장 높다.

그럼 교차검증을 통해 이 분류기의 정확도를 측정해보자.

1
2
3
from sklearn.model_selection import cross_val_score

cross_val_score(sgd, X_train, y_train, scoring = 'accuracy')

array([0.8715, 0.88191667, 0.86891667, 0.8875, 0.88683333])

모든 테스트 폴드에서 87퍼센트 이상의 정확도를 얻었다.

​그럼 이 분류기의 성능을 조금 더 높일 수 있는 방법에 대해 생각해보자.

한 가지 좋은 방법은, 성능을 낮추고있는 에러의 종류를 분석하는 것이다.

​먼저, 이 다중분류기의 오차행렬을 분석해보자

위에서 오차행렬을 만들었던것과 동일하게 coss_val_predict() 함수를 사용해 예측을 만들고 confusion_matrix() 메서드를 통해 오차행렬을 출력해보자

1
2
3
4
5
y_train_pred = cross_val_predict(sgd, X_train, y_train,cv = 3)

conf_matrix = confusion_matrix(y_train, y_train_pred)

plt.matshow(conf_matrix, cmap = plt.cm.gray)

matshow 함수를 사용하면, 오차 행렬을 훨씬 직관적으로 해석 할 수 있다.

​​image1
​​
​​x축이 실제값이고, y축이 예측 값이다. 색이 옅은 부분일수록 큰 숫자이므로주 대각서의 색이 모두 연한것으로 보아 이 모델이 대부분의 클래스를 올바르게 분류하고 있다는것을 알 수 있다.

​숫자 5에 해당하는 부분이 다른 숫자에 비해 조금 어두운데, 이는 5라는 숫자를 다른숫자만큼 잘 분류하지 못한다는 뜻이다.

​근데 이렇게 봐서는 주대각선 클래스의 숫자가 다른 숫자에 비해 크기가 너무커서 거의 다 검은색으로 보일 뿐만 아니라, 에러의 절대개수로만 만들어진 오차 행렬이기 때문에 직관적으로 컬러맵을 해석하기에는 어려움이 있다. 따라서 에러와 크게 관련없는 주대각선은 제거해주고, 클래스의 개수도 절대 개수가 아닌, 에러비율로 바꾸어서 다시 컬러맵을 그려보자

​먼저 오차행렬의 열을 모두 더한 값으로 그 열의 요소 하나씩을 나누어
에러 비율의 오차 행렬로 만들어준다.

1
2
3
sum_rows = conf_matrix.sum(axis = 1, keepdims = True)

conf_matrix = conf_matrix / sum_rows

그리고 주 대각선의 값을 0으로 만들어주자.

1
2
3
np.fill_diagonal(norm_conf_matrix, 0)

plt.matshow(norm_conf_matrix, cmap = plt.cm.gray)

​​image1
​​
​​주 대각선을 0으로 만들어주니 나머지 오류의 컬러맵 색깔이 밝아져, 훨씬 직관적으로 보인다.

​컬러맵을 대략 훑어보면, 실제 데이터 3의 행이 유독 색이 밝은걸 확인 할 수 있다.

3은 2, 5, 8로 잘못 분류된경우가 다른 수에 비해 많았고,

8의 경우는 많은 경우의 3이 8로 잘못 분류되었다.

그밖에도 9를 7, 8로 헷갈려 하는 걸로 보인다.

​이렇게 오차 행렬을 시각화 하면 이 모델의 성능이 어떠한 이유로 저하되고 있는지를 정확히 확인 할 수가 있다.

​그렇다면 이 모델의 성능을 더 높이고 싶다면 어떻게 해야 할까?

​가장 이 모델이 어려워하는 3과 8의 분류를 예로들면. 3과 8만 모아져 있는 데이터의 수를 더 늘려 학습시키거나 분류의 도움이 되는 특성, 동심원을 구해내는 특성을 추가한다거나 하는 방식으로 이를 계산 할 수 있다. 정확히 어떤점에서 이 모델이 분류를 어려워 하는지를

확인하기 위해 한번 직접 이미지를 출력해보자.

1
2
3
4
5
6
7
a = 3 
b =8

X_aa = X_train[(y_train == a)&(y_train_pred == a)]
X_ab = X_train[(y_train == a)&(y_train_pred == b)]
X_ba = X_train[(y_train == b)&(y_train_pred == a)]
X_bb = X_train[(y_train == b)&(y_train_pred == b)]

X_aa는 3을 3이라고 예측한 이미지,

X_ab는 3을 8이라고 예측한 이미지,

X_ba는 8을 3이라고 예측한 이미지.

X_bb는 8을 8이라고 예측한 이미지 이다.

​먼저 X_aa의 이미지들을 출력해보자.

​```python
​plt.figure(figsize= (8,8))

for i in range(0,25):
plt.subplot(5,5,i+1)
plt.imshow(X_aa[i].reshape(28,28), cmap =’binary’)
​```

​이렇게 입력하면 X_aa의 이미지를 5*5로 출력할 수 있는데, 동일한 방법으로 나머지 imshow 안에 있는 값을

X_ab, X_ba, X_bb로 바꾸어 출력해보면,

​​image1
​​
​​이렇게 이미지를 추출해서 보니, 확실히 명확하게 잘 쓴 글씨는 잘 분류한 반면에, 글씨가 끊겨 있거나 너무 삐뚤어진 글씨는 잘 분류하지 못했다.

특히 3을 8이라고 분류한 X_ab의 경우, 숫자가 끊어져있거나 너무 꽉맞게 글씨가 써져있는 경우가 많다.

이러한 이미지들을 바탕으로 우리는 글씨들의 위치를 정가운데에 맞추고, 기울어진 각도나 너무 큰글씨의 스케일을 조정하여 전처리 하면 이 모델의 성능을 개선할 수 있다는 가정을 해 볼 수 있을것이다.

​이처럼 오차행렬을 통한 분석은 이 모델이 어떠한 이유로 성능이 저하되는지를 살펴 볼 수 있기 때문에 아주 유용하다.

이번에는 다중 레이블 분류를 구현해보도록 하자.

지금 까지 했던 분류는 하나의 샘플에 하나의 클래스만 할당되었다.

하지만 만약 하나의 샘플이 여러개의 클래스를 갖는다면 어떻게 해야할까?

예를들어 사진에 있는 사람의 얼굴을 분류하는 모델을 만든다고 할 때, 세명의 얼굴이 있다면 우리는 각각의 얼굴을 분류하여 하나의 사진에 세가지 클래스를 출력해야 할 것 이다.

​우리가 만든 모델을 다중 레이블 분류하도록 만들어보자.

이 숫자가 큰값인지 판별하고, 홀수인지 판별하게 할것이다.

만약 숫자가 7,8,9 중 하나의 값이라면 True를, 아니면 False를 반환하게 할 것 이다.

또, 숫자가 홀수라면 True, 아니라면 False를 반환할 것 이다.

큰값인지 작은값인지의 레이블과 홀수인지 아닌지를 판별하여 두가지 클래스를 출력하므로,

이 모델은 다중레이블 분류 모델이다.

그러면 다중 분류 레이블을 지원하는 KNeighborsClassifier 모델을 사용해서 분류해보자.

1
2
3
4
5
6
7
8
from sklearn.neighbors import KNeighborsClassifier

y_train_large = (y_train>= 7)
y_train_odd = (y_train %2 == 1)
y_multilable = np.c_[y_train_large, y_train_odd]

knn = KNeighborsClassifier()
knn.fit(X_train, y_multilable)

y_multilabel 이라는 두개의 타겟 레이블이 담긴 데이터셋을 훈련시킨다.

KNeborsClassfier는 다중 분류레이블을 지원하므로, 알아서 이 두개의 타겟레이블을 예측할 수 있도록 해준다.

1
2
3
knn.predict([some_digit])

array([[False,  True]])

우리가 테스트 하기위해 만들어뒀던 5의 이미지인 some_digit 변수를 넣어보니 큰값은 False, 홀수는 True가 나왔다. 5는 7,8,9에 속하지도 않고 홀수가 맞으니, 정확하게 예측한 셈이다.

​이렇게 데이터셋을 배열로 통합하여 학습하면, 다중 레이블 분류를 할 수 있다.

이제 마지막으로 다중 출력 다중 클래스 분류 (간단하게 다중 출력 모델이라고 하자.) 모델을 만들어보자.

다중 레이블 분류에서 각 레이블의 클래스가 두개 이상의 값을 가질 수 있는 방식의 모델을 다중 출력 모델이라 한다.

​이번엔 데이터셋에 노이즈를 주어 선명하지 않은 이미지 파일을 만들어 보고, 이 파일을 학습시켜 선명한 이미지를 예측하는 모델을 만들어 볼 것이다.

각 픽셀이 레이블이고, 각 픽셀의 0-255의 값을 가지므로, 이 모델은 다중 출력 모델이다.

먼저 데이터에 노이즈를 섞어주면,

1
2
3
4
5
6
7
8
9
10
11
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
[출처] 사이킷런 (2) - 손글씨 분류 모델|작성자 lmj938

noise 는 randint를 사용해 임의의 1-100사이의 값을 랜덤으로 호출해준뒤,

그 값들을 X_train의 shape인 (6000,784)의 값으로 반환해준다.

그리고 X_train값에 노이즈를 합쳐주면, 노이즈가 낀 이미지 데이터셋이 생성되는데,

some_digit의 이미지를 출력해보면 확실히 노이즈가 낀 이미지가 생겼음을 확인 할 수 있다.

1
plt.imshow(X_train_mod[0].reshape(28,28), cmap = 'binary')

​​image1
​​
​​X_train에 임의로 노이즈가 낀 X_train_mod를 만들었으니 기존의 노이즈가 없던 X_train이 y_train_mod라는 이름의 타겟이 된다.

그럼 이 데이터셋을 학습시켜 선명한 이미지를 예측하여 출력하는지 확인 해보자.

1
plt.imshow(knn.predict([X_train_mod[0]]).reshape(28,28), 'binary')

​​image1
​​
​​성능이 그렇게 좋지않아 글씨가 좀 훼손되었지만, 노이즈는 깔끔하게 제거 된것을 확인 할 수 있다.

데이터를 더 넣어주거나 전처리를 통해 성능을 높이면 훨씬 더 깔끔한 이미지를 얻을 수 있을것이다.

​여기까지 Mnist 데이터셋을 통해 분류모델에 대해 공부해보았다.

​이것으로 내가 실무에서 머신러닝을 사용해 유의미한 결과를 낼 수 있다기보다는, 머신러닝이라는게 어떠한 과정을 통해서 이루어지는지 알 수있었다는것에 의의를 두고 공부해보았다.

​데이터를 전처리하는 과정이 재밌기도 하면서, 책에서 전처리를 어떻게 해야할지 알려주기 때문에 수월하게 한것이지, 완전히 raw한 데이터 속에서 이런 전처리를 생각해내는건 굉장히 어려운일이겠다 싶었다…

​그 수준까지 가기 위해서는 더 많은 공부가 필요할 것 같다.

댓글남기기