열렬히.뛰기

3. 분류

머신러닝 & 딥러닝 > 핸즈온 머신러닝 2 > 3. 분류

1. MNIST

기본 설정 : plot_digit(), plot_digits()

그림을 그리기 위해 책에서 만들어준 함수.

python
def plot_digit(data):
    image = data.reshape(28, 28)
    plt.imshow(image, cmap = mpl.cm.binary,
               interpolation="nearest")
    plt.axis("off")
python
# 숫자 그림을 위한 추가 함수
def plot_digits(instances, images_per_row=10, **options):
    size = 28
    images_per_row = min(len(instances), images_per_row)
    # n_rows = ceil(len(instances) / images_per_row) 와 동일합니다:
    n_rows = (len(instances) - 1) // images_per_row + 1

    # 필요하면 그리드 끝을 채우기 위해 빈 이미지를 추가합니다:
    n_empty = n_rows * images_per_row - len(instances)
    padded_instances = np.concatenate([instances, np.zeros((n_empty, size * size))], axis=0)

    # 배열의 크기를 바꾸어 28×28 이미지를 담은 그리드로 구성합니다:
    image_grid = padded_instances.reshape((n_rows, images_per_row, size, size))

    # 축 0(이미지 그리드의 수직축)과 2(이미지의 수직축)를 합치고 축 1과 3(두 수평축)을 합칩니다. 
    # 먼저 transpose()를 사용해 결합하려는 축을 옆으로 이동한 다음 합칩니다:
    big_image = image_grid.transpose(0, 2, 1, 3).reshape(n_rows * size,
                                                         images_per_row * size)
    # 하나의 큰 이미지를 얻었으므로 출력하면 됩니다:
    plt.imshow(big_image, cmap = mpl.cm.binary, **options)
    plt.axis("off")

Dataset 불러오기

  • 분류 예시에 가장 많이 쓰는 MNIST 데이터셋 불러오기
python
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)
print(mnist.keys())
X, y = mnist["data"], mnist["target"]
X.shape
y.shape

데이터셋의 구조

  1. DESCR 키 : 데이터셋 설명
  2. data 키 : 1개 행, 1개 열로 이뤄진 배열을 가짐.
  3. target 키 : 레이블 배열을 담음.

즉, X = ‘그림’이고 y = ‘그림에 딸린 레이블’이다.

  • 데이터셋에서 이미지 하나 확인해 보기
    1. 28x28 행렬로 재구성
    2. matplotlib으로 이를 실행시키면하면
python
import matplotlib as mpl
import matplotlib.pyplot as plt

some_digit = X[0]

some_digit_image = some_digit.reshape(28, 28)
plt.imshow(some_digit_image, cmap="binary")
plt.axis("off")
plt.show()
만약 에러코드가 난다면?
  • 타입 변경이 필요.
python
X = X.to_numpy()
  • 이후 다시 실행
python
import matplotlib as mpl
import matplotlib.pyplot as plt

some_digit = X[0]
some_digit = some_digit.values
# X가 판다스 data.frame이라 넘파이 배열로 바꿔야 함.

some_digit_image = some_digit.reshape(28, 28)
plt.imshow(some_digit_image, cmap="binary")
plt.axis("off")
plt.show()

어쨌든 실행 시 다음과 같은 이미지가 나온다.

하나의 작은 네모들이 모여 5가 되었다.

python
# 실제로 0번째가 5로 레이블링 되어 있는가?
import numpy as np
print(y[0])

>>> 5

python
# 모든 레이블을 숫자로 바꾼다.
y = y.to_numpy() # y도 df이므로 np로 바꿈.

import numpy as np
y = y.astype(np.uint8)
print(y)

>>> [5 0 4 ... 4 5 6]

테스트 세트 만들기

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

2. 이진 분류기

문제를 단순화 해 숫자 5만 식별하는 ‘감지기’를 만든다고 치자.

이 감지기는 yes / no만 식별할 수 있으므로 ‘이진 분류기’이다.

  • 분류작업을 위한 타깃 벡터
    • 훈련세트 중 5인 애들만 뽑아내기.
python
# 분류 작업을 위한 타겟 벡터
X_train_5 = (X_train == 5)
y_train_5 = (y_train == 5)

확률적 경사 하강법 분류기를 이용해 분류

  • 매우 큰 데이터 처리에 유용
  • 사이킷런의 SGDClassifier 클래스를 사용하면 됨.
python
from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(random_state=42) # random_state 고정으로 값 불변
sgd_clf.fit(X_train, y_train_5)


# sgd_clf를 사용해 some_digit이 5가 맞는지 확인
sgd_clf.predict([some_digit])

>>> array([ True]) 라고 나온다.

3. 성능 측정

분류기를 평가하는 작업. 많은 성능지표가 있다.

(1) 교차 검증을 통한 정확도 측정

  • cross_val_score()로 교차검증 실행.
단, cross_val_score()보다 더 많이 교차 검증과정을 제어할 때는 StratifiedKFold() 를 사용한다.
python
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

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

for train_index, test_index in skfolds.split(X_train, y_train_5):
    clone_clf = clone(sgd_clf)
    X_train_folds = X_train[train_index]
    y_train_folds = y_train_5[train_index]
    X_test_fold = X_train[test_index]
    y_test_fold = y_train_5[test_index]
    
    clone_clf.fit(X_train_folds, y_train_folds)
    y_pred = clone_clf.predict(X_test_fold)
    n_correct = sum(y_pred == y_test_fold)
    print(n_correct / len(y_pred)) 
python
# 실제 출력 결과
0.9669
0.91625
0.96785
  • cross_val_score() 함수로 폴드가 3개인 k-겹 교차 검증을 사용해 SGDClassifier 모델을 평가
python
from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
python
# 결과는 다음과 같다.
array([0.95035, 0.96035, 0.9604 ])

# 정확도: 95% 이상!
  • 이번에는 모든 이미지를 ‘5 아님’으로 분류하는 ‘더미 분류기’를 만들어 비교
python
# 더미 분류기 만들기
from sklearn.base  import BaseEstimator

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

# 모델의 정확도를 추측
never_5_clf = Never5Classifier()
cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
python
# 결과는 다음과 같다.
array([0.91125, 0.90855, 0.90915])

# 정확도 : 약 90%
  • 정확도가 계속 90% 이상 으로 나온다. 뭔가 이상하다.
  • 따라서 정확도만 가지고 분류기의 성능 측정 지표로 사용하지 않음.

(2) 오차 행렬

  • 오차행렬이란?

    클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 것.

    ex. 숫자 5를 3으로 잘못 분류된 횟수 → 오차행렬(5,3) 을 보면 됨.

  • 미리 예측 값을 만들어야 한다.

    • 테스트 세트를 이용해 만들 수 있지만, 그럴수 없다.
    • 테스트 세트는 마지막에 사용하는 것.
  • 따라서 cross_val_predict()가 그 역할을 대신한다.

python
# cross_val_predict(): # k-겹 교차검증 수행, 각 테스트 폴드의 예측을 반환한다.
# 예측값을 대신해 주는 역할
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)

# 오차행렬 만들기
from sklearn.metrics import confusion_matrix
print(confusion_matrix(y_train_5, y_train_pred), end='\n')

# 완벽한 분류기면 대각선을 제외한 나머지가 전부 0!
y_train_perfect_predictions = y_train_5
print(confusion_matrix(y_train_5, y_train_perfect_predictions))
python
# 오차행렬 자체가 일종의 가설검정
# 오차행렬 결과
[[53892   687]
 [ 1891  3530]]
"""
(1,1) = not 5 -> not 5 = 53892  (진짜 음성, TN)
(1,2) = not 5 -> 5     = 687    (거짓 음성, FP)
(2,1) = 5 -> not 5     = 1891   (거짓 양성, FN)
(2,2) = 5 -> 5         = 3530   (진짜 양성, TP)
"""

# 완벽한 분류기의 오차행렬.
[[54579     0]
 [    0  5421]]

"""
(1,1) = not 5 -> not 5 = 54579  (진짜 음성, TN)
(1,2) = not 5 -> 5     = 0      (거짓 음성, FP)
(2,1) = 5 -> not 5     = 0      (거짓 양성, FN)
(2,2) = 5 -> 5         = 5421   (진짜 양성, TP)
"""
  • 사실 통계적 유의 검정(1종 오류, 2종 오류)와 같다.

(3) 정밀도와 재현율

  • 오차행렬보다 더 요약된 정보가 필요할 때 사용한다.

  • 다음과 같은 정의를 가진다.

    1. 정밀도

      \dfrac {TP}{TP + FP}, (TP = 진짜 양성, FP = 거짓 양성)

      정밀도 = 양성으로 예측된 것 중 진짜의 비율은?

    2. 재현율

      \dfrac {TP}{TP + FN}, (TP = 진짜 양성, FN = 거짓 음성)

      재현율 = 진짜로 양성인 것 중에서 제대로 분류된 비율은?

      민감도, 진짜 양성 비율이라고도 한다.

  • 사이킷런에서 정밀도 , 재현율을 계산하는 함수를 제공한다.

    python
    print("재현율:", recall_score(y_train_5, y_train_pred))
    
    python
    # 결과
    >>> 정밀도: 0.8370879772350012
    >>> 재현율: 0.6511713705958311
    
  • F1점수: 정밀도와 재현율의 조화평균

    \mathsf F_{1} = \displaystyle\dfrac {2} {\textstyle \dfrac{1}{\text{정밀도}} + \dfrac{1}{\text{재현율}}} = 2 \times \dfrac {\text{정밀도} \times \text{재현율}}{\text{정밀도} + \text{재현율}} = \displaystyle\frac {TP} {{TP} + \displaystyle\frac{FN+FP}{2}}
    python
    from sklearn.metrics import f1_score
    f1_score(y_train_5, y_train_pred)
    

    >>> 0.7325171197343846

  • 정밀도와 재현율이 비슷하다면 점수가 높음.

    • 그러나, 점수가 높다고 꼭 좋은 것은 아님.
    • 정밀도와 재현율은 서로 반비례 관계.

(4) 정밀도/재현율 트레이드오프

  • SGDClassifier → 결정함수를 사용하여 각 샘플의 점수 계산

    • 임계값 초과 : 양성 클래스
    • 임계값 미만 : 음성 클래스

  • 임계값이 오른쪽 화살표일때 (9와 5 사이)

    • 화살표 오른쪽 진짜 5의 갯수 : 6개
    • 화살표 오른쪽 숫자의 갯수 : 8개
    • 전체 범위 진짜 5의 갯수: 6개

    따라서, 정밀도는 6/8 & 재현율은 6/6

  • 임계값이 가운데 화살표일때

    • 화살표 오른쪽 진짜 5의 갯수 : 4개
    • 화살표 오른쪽 숫자의 갯수 : 5개
    • 전체 범위 진짜 5의 갯수: 6개

    따라서, 정밀도는 4/5 & 재현율은 4/6

  • 임계값이 왼쪽 화살표일때

    • 화살표 오른쪽 진짜 5의 갯수 : 3개
    • 화살표 오른쪽 숫자의 갯수 : 3개
    • 전체 범위 진짜 5의 갯수: 6개

    따라서, 정밀도는 4/5 & 재현율은 4/6

여기서 알 수 있는 것.

  • 임계값이 올라가면 정밀도는 올라가나, 재현율은 떨어진다.
  • 임계값이 내려가면 정밀도는 내려가나, 재현율은 올라간다.
  • 정밀도와 재현율은 반비례하다.

임계값을 직접 지정할 순 없어도 예측에 사용된 점수는 확인 가능.

python
# 예측에 사용된 점수 보기
y_scores = sgd_clf.decision_function([some_digit])
print("y_scores", y_scores)

threshold = 0
y_some_digit_pred = (y_scores > threshold)
print("임계값 > 0일때:", y_some_digit_pred)

threshold = 8000
y_some_digit_pred = (y_scores > threshold)
print("임계값 > 8000일때:", y_some_digit_pred)
python
# 결과
y_scores [2164.22030239]
임계값 > 0일때: [ True]
임계값 > 8000일때: [False]

어떻게 적절한 임계값을 구할 것인가?

python
# 결정 점수를 반환받도록 지정
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
                            method = "decision_function")

# 받은 결정점수로 정밀도, 재현율, 임계값 지정가능
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)


# 정밀도, 재현율, 임계값 그리기
def plot_precision_recall_vs_thresholds(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="정밀도")
    plt.plot(thresholds, recalls[:-1], "g--", label="재현율")
    plt.legend("center left")

plot_precision_recall_vs_thresholds(precisions, recalls, thresholds)
plt.show()

그래프 한글 폰트 출력 >> 클릭

  • 정밀도는 울퉁불퉁한 곡선이 그려졌다.
    • 보통 정밀도와 임계값은 비례.
  • 그러나 항상 그런 것은 아니다.
  • 그래프처럼 정밀도가 낮아질 수도 있다.
python
# 재현율에 대한 정밀도 곡선
def pp_vs_re(precisions, recalls):
    plt.plot(recalls, precisions, "b--", linewidth=2)
    plt.xlabel("Recall", fontsize=16)
    plt.ylabel("Precision", fontsize=16)
    plt.axis([0,1,0,1])
    plt.grid(True)
    
plt.figure(figsize=(8,6))
pp_vs_re(precisions, recalls)
plt.plot([0.4386, 0.4386], [0., 0.9], "r:")
plt.plot([0.0, 0.4386], [0.9, 0.9], "r:")
plt.show()

트레이드오프를 선택할 가장 좋은 방법이다.

  • 재현율이 80% 부터 급격히 줄어들고 있음.
  • 하강점을 트레이드오프로 삼는 것이 좋음.

만약 정밀도 90%가 목표라면?

python
# 90% 정밀도가 되는 가장 낮은 임계값을 찾는다.
thre_90_pre = thresholds[np.argmax(precisions >= 0.9)] 
print(thre_90_pre) 

# 이를 기반으로 예측 실행 
ytrain_pred90 = (y_scores >= thre_90_pre)

# 이때의 정밀도 = 목표달성
print(precision_score(y_train_5, ytrain_pred90))

# 이때의 재현율
print(recall_score(y_train_5, ytrain_pred90))
python
정밀도 90% 일 때 임계값 = 3370.0194991439594
정밀도 = 0.9000345901072293
재현율 = 0.4799852425751706

(5) ROC 곡선

  • ROC = Receiver Operating Characteristic = 수신기 조작 특성
ROC = \dfrac {FPR}{TPR} = \dfrac {1 - TNR}{TPR} \\ [10pt] FPR = \text{(거짓 양성 비율)} = 1 - TNR = 1 - \text{(진짜 음성 비율)} \\ [10pt] TPR = \text{(진짜 양성 비율)}
  • 값 계산 & 그래프
python
# fpr, tpr, 임계값 계산
from sklearn.metrics import roc_curve
fpr, tpr, threshold = roc_curve(y_train_5, y_scores)

# tpr에 대한 fpr 곡선
def plot_roc_curve(fpr, tpr, label=None):
    plt.plot(fpr, tpr, linewidth=2, label=label)
    plt.plot([0,1], [0,1], 'k--')
    plt.xlabel('FPR')
    plt.ylabel('TPR')
    plt.grid(True)

plt.figure(figsize=(8, 6))
plot_roc_curve(fpr, tpr)
fpr_90 = fpr[np.argmax(tpr >= recall_90_precision)]
plt.plot([4.837e-3, 4.837e-3], [0., 0.4368], "r:")
plt.plot([0.0, 4.837e-3], [0.4368, 0.4368], "r:")
plt.plot([4.837e-3], [0.4386], "ro")
plt.show()

여기서도 트레이드오프가 존재

  • TPR 이 높을수록, FPR이 늘어남.

  • 점선 : 완벽한 랜덤 분류기의 ROC 곡선

  • 좋은 분류기는 이 점선에서 최대한 멀어져야 함.

  • 곡선 아래의 면적(Area Under the Curve = AUC) 측정으로 분류기들을 비교

    • 완벽한 분류기 : ROC의 AUC가 1
    • 완전한 랜덤 분류기 : 0.5
python
# 사이킷런의 roc_auc_score >> AUC 계산 함수
from sklearn.metrics import roc_auc_score as ras
print(ras(y_train_5, y_scores))

>>> 0.9604938554008616

  • ROC 곡선 vs ROC AUC 점수
    • 랜덤포레스트로 SGDClassifier의 ROC 곡선과 ROC AUC 점수 비교
python
from sklearn.ensemble import RandomForestClassifier

forest_clf = RandomForestClassifier(random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5,
 cv=3, method="predict_proba")

# 양성 그래프에 대한 확률을 점수로 사용
y_scores_forest = y_probas_forest[:, 1]
fpr_f, tpr_f, thre_f = roc_curve(y_train_5, y_scores_forest)

# 이를 기반으로 그래프 그리기
recall_for_forest = tpr_f[np.argmax(fpr_f >= fpr_90)]

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.plot([fpr_90, fpr_90], [0., recall_90_precision], "r:")
plt.plot([0.0, fpr_90], [recall_90_precision, recall_90_precision], "r:")
plt.plot([fpr_90], [recall_90_precision], "ro")
plt.plot([fpr_90, fpr_90], [0., recall_for_forest], "r:")
plt.plot([fpr_90], [recall_for_forest], "ro")
plt.grid(True)
plt.legend(loc="lower right", fontsize=16)

  • SGD = 파란색 점선
  • 랜덤포레스트 = 파란색 실선
  • 랜덤포레스트의 성능이 더 좋다.
  • 랜덤포레스트 ROC 곡선이 왼쪽 위 모서리에 더 가깝기 때문.

이때의 정밀도와 재현율 점수를 계산해보자.

python
print(ras(y_train_5, y_scores_forest))

>>> 0.9983436731328145 : 정밀도 점수. 굉장히 좋다.

4. 다중 분류

  • 이진 분류: 두개의 클래스만 분류 가능
  • 다중 분류: n개의 클래스 분류 가능

분류기의 종류에 따른 다중 분류

  • 다중 분류 가능 → 확률적 경사하강(SGD) 분류기, 랜덤 포레스트 분류기, 나이브 베이즈
  • 이진 분류 가능 → 로지스틱 회귀, 서포트 벡터 머신(SVM)
  • 다중 분류 작업에 이진 분류기를 사용할 경우, 사이킷런이 자동으로 OvR, OvO를 실행함.

OVR vs OVO

OVR(One-versus-the-rest)

  • 각 분류기의 결정 점수 중 가장 높은 것을 클래스로 선택.
  • ex) 숫자 분류기 10개를 훈련해 클래스가 10개인 숫자 이미지 분류

OVO(One-versus-one)

  • 각 숫자 조합마다 이진 분류기를 훈련시키는 것.
  • ex) (0,1), (0,2), (0,3), \dots, (9,10)
  • n개의 클래스 → n(n-1) \div 2개의 분류기 필요.

서포트 벡터 머신(SVC) >> 다중분류 실행

아까 본 대로 SVC은 이진 분류만 가능. 이 경우 자동으로 OvO 실행

python
from sklearn.svm import SVC

# 타깃 클래스 y_train_5 대신 원래 클래스 사용.
# 예측 성공 여부 판별.
svm_clf = SVC()
svm_clf.fit(X_train, y_train)
print(svm_clf.predict([some_digit]))

# 정말 잘 분류했는지 확인해보기
svm_clf_df = svm_clf.decision_function([some_digit])
print(svm_clv_df)
python
# 예측 하나를 만드는 데 성공.
array([5], dtype=uint8)

# 샘플 당 10개의 점수를 반환. 가장 높은 점수가 클래스 5에 해당.
[[ 1.72501977  2.72809088  7.2510018   8.3076379  -0.31087254  9.3132482
   1.70975103  2.76765202  6.23049537  4.84771048]]
python
# 가장 높은 점수가 뭔지 보기

import numpy as np
# 가장 높은 점수가 속해 있는 클래스
print(np.argmax(s_d_s)) 

# 전체 클래스 나열
print(svm_clf.classes_)

# 5번 클래스의 값
print(svm_clf.classes_[5])
python
# 가장 높은 점수가 속해 있는 클래스
5

# 전체 클래스 나열
[0 1 2 3 4 5 6 7 8 9]

# 5번 클래스의 값 (여기서는 클래스의 인덱스 = 클래스의 값)
5

서포트 벡터 머신(SVC) >> 다중분류 실행 with OvR

  • SVC은 기본적으로 OvO. 여기서는 OvR 쓰도록 강제하기.
  • 분류기에 OnevsOneClassifier 또는 OneVsRestClassifier를 쓰면 강제로 OvO나 OvR 적용 가능.
python
from sklearn.multiclass import OneVsRestClassifier
ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
ovr_clf.fit(X_train[:1000], y_train[:1000])
print(ovr_clf.predict([some_digit]))
print(len(ovr_clf.estimators_))
python
>>> array([5], dtype=uint8)
>>> 10

확률적 경사하강(SGD) >> 다중분류 훈련

  • 딱히 OvR이나 OvO가 필요 없음.
python
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
python
array([3], dtype=uint8)

SGD 분류기가 클래스마다 부여한 점수 확인

python
# sgd가 클래스마다 부여한 점수 확인
print(sgd_clf.decision_function([some_digit]))
python
[[-31893.03095419 -34419.69069632  -9530.63950739   1823.73154031
  -22320.14822878  -1385.80478895 -26188.91070951 -16147.51323997
   -4604.35491274 -12050.767298  ]]

# 대부분 음수인데, 클래스 3의 점수만 양수. 의심할만 하다.

교차 검증 시도

python
# sgd의 정확도 체크
print(cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy"))
python
[0.87365 0.85835 0.8689 ]

# 모든 테스트 폴드에서 84% 이상으로 점수가 나옴.

입력의 스케일을 조정해 점수를 좀 더 높여보자.

python
# 성능 더 높여보기
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")
python
[0.8983 0.891  0.9018]

# 스케일이 높아진 만큼 값 도출에 시간이 더 걸린다.
# 값 도출에 7분 정도 걸림.

5. 에러분석

모델을 하나 정한상태에서, 모델의 성능을 향상 시킬 방법을 생각해보자.

에러를 분석해 개선하는 것이 좋은 방법일 수 있다.

에러 분석에 좋은 방법 중 하나는 오차행렬을 보는 것이다.

python
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
conf_mx = confusion_matrix(y_train, y_train_pred)
print(conf_mf)

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

실제로 도출된 오차행렬과 이미지를 살펴보면 다음과 같다.

python
array([[5577,    0,   22,    5,    8,   43,   36,    6,  225,    1],
       [   0, 6400,   37,   24,    4,   44,    4,    7,  212,   10],
       [  27,   27, 5220,   92,   73,   27,   67,   36,  378,   11],
       [  22,   17,  117, 5227,    2,  203,   27,   40,  403,   73],
       [  12,   14,   41,    9, 5182,   12,   34,   27,  347,  164],
       [  27,   15,   30,  168,   53, 4444,   75,   14,  535,   60],
       [  30,   15,   42,    3,   44,   97, 5552,    3,  131,    1],
       [  21,   10,   51,   30,   49,   12,    3, 5684,  195,  210],
       [  17,   63,   48,   86,    3,  126,   25,   10, 5429,   44],
       [  25,   18,   30,   64,  118,   36,    1,  179,  371, 5107]])

  • 주대각선 기준, 더 밝은 부분이 잘 인식되었다는 뜻.

  • (5,5) → 조금 더 어둡다. [실제 값도 4444.]

    • 5의 이미지가 적거나, 5를 다른 숫자만큼 잘 분류하지 못한다는 뜻.
  • 에러부분에 초점을 맞추기

    (오차행렬의 각 값) \div (클래스의 이미지 갯수) = 에러 비율.

    이 에러 비율을 비교하자.

에러 부분에 초점 맞추기

python
# (오차행렬의 각 값) / (클래스의 이미지 갯수) = 에러 비율
row_sums = confu_mat.sum(axis=1, keepdims=True)
norm_conf_mx = confu_mat / row_sums

# 다른 항목은 그대로 하고, 주대각선만 0으로 채워 다시 그리기
np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()

  • 행: 실제 클래스

  • 열: 예측한 클래스

  • 많은 이미지들이 8로 인식되고 있음.

  • (3,5)와 (5,3)은 어느 정도 회색.

    3이 5로, 5가 3으로 혼동됨을 보여줌.

    인제 3,5 에 포커스를 맞춰보자.

3과 5에 포커스를 맞춰보기

우선 plot_digits() 를 추가한다.

python
# 숫자 그림을 위한 추가 함수
def plot_digits(instances, images_per_row=10, **options):
    size = 28
    images_per_row = min(len(instances), images_per_row)
    # n_rows = ceil(len(instances) / images_per_row) 와 동일합니다:
    n_rows = (len(instances) - 1) // images_per_row + 1

    # 필요하면 그리드 끝을 채우기 위해 빈 이미지를 추가합니다:
    n_empty = n_rows * images_per_row - len(instances)
    padded_instances = np.concatenate([instances, np.zeros((n_empty, size * size))], axis=0)

    # 배열의 크기를 바꾸어 28×28 이미지를 담은 그리드로 구성합니다:
    image_grid = padded_instances.reshape((n_rows, images_per_row, size, size))

    # 축 0(이미지 그리드의 수직축)과 2(이미지의 수직축)를 합치고 축 1과 3(두 수평축)을 합칩니다. 
    # 먼저 transpose()를 사용해 결합하려는 축을 옆으로 이동한 다음 합칩니다:
    big_image = image_grid.transpose(0, 2, 1, 3).reshape(n_rows * size,
                                                         images_per_row * size)
    # 하나의 큰 이미지를 얻었으므로 출력하면 됩니다:
    plt.imshow(big_image, cmap = mpl.cm.binary, **options)
    plt.axis("off")

그리고 이미지를 획득.

python
cl_a, cl_b = 3, 5
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]

plt.figure(figsize=(8,8))
plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)
plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)
plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)
plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)
plt.show()

  • 몇개는 에러가 났다.
  • 다만 선형분류기의 특성 상 어쩔 수 없는 면이 있다.

선형분류기는 픽셀에 가중치를 할당한 뒤 강도의 가중치 합을 클래스 점수로 계산.

따라서 3과 5는 몇 개의 픽셀만 다르기에 모델이 쉽게 혼동하게 된다.

6. 다중 레이블 분류

분류기가 샘플마다 여러개의 클래스를 출력하는 것.

ex) 같은 사진 속 여러 사람이 등장하는 케이스

  • [’John 있음’ , ‘Steve 없음’, ‘William 있음’]= [1, 0, 1]
  • [’John 없음’ , ‘Steve 있음’, ‘William 있음’]= [0, 1, 1]
  • 모아보면 […, [1, 0, 1], [0, 1, 1], …]

각 숫자 이미지에 2개의 타깃 레이블이 담긴 y_multilabel 배열 제작

  • 1번째는 숫자가 큰 값인지 나타내고, 2번째는 홀수인지 나타냄.
python
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)
  • KNeighborsClassifier 인스턴스를 만들어낸 후 다중 타깃 배열로 훈련
python
knn_clf.predict([some_digit])

>>> array([[False, True]])

  • 올바르게 분류 완료!
  • 5는 크지 않고(False), 홀수임(True).

다중 레이블 f1점수로 평가하기

  • 여기서는 모든 레이블에 대한 F_{1} 점수의 평균을 계산
python
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
f1_score(y_multilabel, y_train_knn_pred, average="macro")

>>> 0.976410265560605

7. 다중 출력 분류

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

잡음 제거 시스템

  • 이미지에 잡음 추가하기
python
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
  • 이미지 비교
python
some_index = 0
plt.subplot(121); plot_digits(X_test_mod[some_index])
plt.subplot(122); plot_digits(y_test_mod[some_index])
plt.show()

  • 왼쪽 : 잡음 섞인 이미지

  • 오른쪽 : 깨끗한 타깃 이미지

  • 분류기를 통해 깨끗하게 만든 결과

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

  • 타깃과 굉장히 비슷하다.