1. MNIST
기본 설정 : plot_digit(), plot_digits()
그림을 그리기 위해 책에서 만들어준 함수.
def plot_digit(data):
image = data.reshape(28, 28)
plt.imshow(image, cmap = mpl.cm.binary,
interpolation="nearest")
plt.axis("off")
# 숫자 그림을 위한 추가 함수
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 데이터셋 불러오기
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
데이터셋의 구조
- DESCR 키 : 데이터셋 설명
- data 키 : 1개 행, 1개 열로 이뤄진 배열을 가짐.
- target 키 : 레이블 배열을 담음.
즉, X = ‘그림’이고 y = ‘그림에 딸린 레이블’이다.
- 데이터셋에서 이미지 하나 확인해 보기
- 28x28 행렬로 재구성
- matplotlib으로 이를 실행시키면하면
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()
만약 에러코드가 난다면?
- 타입 변경이 필요.
X = X.to_numpy()
- 이후 다시 실행
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가 되었다.
# 실제로 0번째가 5로 레이블링 되어 있는가?
import numpy as np
print(y[0])
>>> 5
# 모든 레이블을 숫자로 바꾼다.
y = y.to_numpy() # y도 df이므로 np로 바꿈.
import numpy as np
y = y.astype(np.uint8)
print(y)
>>> [5 0 4 ... 4 5 6]
테스트 세트 만들기
X_train, X_test = X[:60000], X[60000:]
y_train, y_test = y[:60000], y[60000:]
2. 이진 분류기
문제를 단순화 해 숫자 5만 식별하는 ‘감지기’를 만든다고 치자.
이 감지기는 yes / no만 식별할 수 있으므로 ‘이진 분류기’이다.
- 분류작업을 위한 타깃 벡터
- 훈련세트 중 5인 애들만 뽑아내기.
# 분류 작업을 위한 타겟 벡터
X_train_5 = (X_train == 5)
y_train_5 = (y_train == 5)
확률적 경사 하강법 분류기를 이용해 분류
- 매우 큰 데이터 처리에 유용
- 사이킷런의 SGDClassifier 클래스를 사용하면 됨.
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() 를 사용한다.
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))
# 실제 출력 결과
0.9669
0.91625
0.96785
- cross_val_score() 함수로 폴드가 3개인 k-겹 교차 검증을 사용해 SGDClassifier 모델을 평가
from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
# 결과는 다음과 같다.
array([0.95035, 0.96035, 0.9604 ])
# 정확도: 95% 이상!
- 이번에는 모든 이미지를 ‘5 아님’으로 분류하는 ‘더미 분류기’를 만들어 비교
# 더미 분류기 만들기
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")
# 결과는 다음과 같다.
array([0.91125, 0.90855, 0.90915])
# 정확도 : 약 90%
- 정확도가 계속 90% 이상 으로 나온다. 뭔가 이상하다.
- 따라서 정확도만 가지고 분류기의 성능 측정 지표로 사용하지 않음.
(2) 오차 행렬
-
오차행렬이란?
클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 것.
ex. 숫자 5를 3으로 잘못 분류된 횟수 → 오차행렬(5,3) 을 보면 됨.
-
미리 예측 값을 만들어야 한다.
- 테스트 세트를 이용해 만들 수 있지만, 그럴수 없다.
- 테스트 세트는 마지막에 사용하는 것.
-
따라서 cross_val_predict()가 그 역할을 대신한다.
# 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))
# 오차행렬 자체가 일종의 가설검정
# 오차행렬 결과
[[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) 정밀도와 재현율
-
오차행렬보다 더 요약된 정보가 필요할 때 사용한다.
-
다음과 같은 정의를 가진다.
-
정밀도
\dfrac {TP}{TP + FP}, (TP = 진짜 양성, FP = 거짓 양성)
정밀도 = 양성으로 예측된 것 중 진짜의 비율은?
-
재현율
\dfrac {TP}{TP + FN}, (TP = 진짜 양성, FN = 거짓 음성)
재현율 = 진짜로 양성인 것 중에서 제대로 분류된 비율은?
민감도, 진짜 양성 비율이라고도 한다.
-
-
사이킷런에서 정밀도 , 재현율을 계산하는 함수를 제공한다.
pythonprint("재현율:", 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}}pythonfrom 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
여기서 알 수 있는 것.
- 임계값이 올라가면 정밀도는 올라가나, 재현율은 떨어진다.
- 임계값이 내려가면 정밀도는 내려가나, 재현율은 올라간다.
- 정밀도와 재현율은 반비례하다.
임계값을 직접 지정할 순 없어도 예측에 사용된 점수는 확인 가능.
# 예측에 사용된 점수 보기
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)
# 결과
y_scores [2164.22030239]
임계값 > 0일때: [ True]
임계값 > 8000일때: [False]
어떻게 적절한 임계값을 구할 것인가?
# 결정 점수를 반환받도록 지정
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()
그래프 한글 폰트 출력 >> 클릭
- 정밀도는 울퉁불퉁한 곡선이 그려졌다.
- 보통 정밀도와 임계값은 비례.
- 그러나 항상 그런 것은 아니다.
- 그래프처럼 정밀도가 낮아질 수도 있다.
# 재현율에 대한 정밀도 곡선
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%가 목표라면?
# 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))
정밀도 90% 일 때 임계값 = 3370.0194991439594
정밀도 = 0.9000345901072293
재현율 = 0.4799852425751706
(5) ROC 곡선
- ROC = Receiver Operating Characteristic = 수신기 조작 특성
- 값 계산 & 그래프
# 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
# 사이킷런의 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 점수 비교
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 곡선이 왼쪽 위 모서리에 더 가깝기 때문.
이때의 정밀도와 재현율 점수를 계산해보자.
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 실행
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)
# 예측 하나를 만드는 데 성공.
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]]
# 가장 높은 점수가 뭔지 보기
import numpy as np
# 가장 높은 점수가 속해 있는 클래스
print(np.argmax(s_d_s))
# 전체 클래스 나열
print(svm_clf.classes_)
# 5번 클래스의 값
print(svm_clf.classes_[5])
# 가장 높은 점수가 속해 있는 클래스
5
# 전체 클래스 나열
[0 1 2 3 4 5 6 7 8 9]
# 5번 클래스의 값 (여기서는 클래스의 인덱스 = 클래스의 값)
5
서포트 벡터 머신(SVC) >> 다중분류 실행 with OvR
- SVC은 기본적으로 OvO. 여기서는 OvR 쓰도록 강제하기.
- 분류기에 OnevsOneClassifier 또는 OneVsRestClassifier를 쓰면 강제로 OvO나 OvR 적용 가능.
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_))
>>> array([5], dtype=uint8)
>>> 10
확률적 경사하강(SGD) >> 다중분류 훈련
- 딱히 OvR이나 OvO가 필요 없음.
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
array([3], dtype=uint8)
SGD 분류기가 클래스마다 부여한 점수 확인
# sgd가 클래스마다 부여한 점수 확인
print(sgd_clf.decision_function([some_digit]))
[[-31893.03095419 -34419.69069632 -9530.63950739 1823.73154031
-22320.14822878 -1385.80478895 -26188.91070951 -16147.51323997
-4604.35491274 -12050.767298 ]]
# 대부분 음수인데, 클래스 3의 점수만 양수. 의심할만 하다.
교차 검증 시도
# sgd의 정확도 체크
print(cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy"))
[0.87365 0.85835 0.8689 ]
# 모든 테스트 폴드에서 84% 이상으로 점수가 나옴.
입력의 스케일을 조정해 점수를 좀 더 높여보자.
# 성능 더 높여보기
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")
[0.8983 0.891 0.9018]
# 스케일이 높아진 만큼 값 도출에 시간이 더 걸린다.
# 값 도출에 7분 정도 걸림.
5. 에러분석
모델을 하나 정한상태에서, 모델의 성능을 향상 시킬 방법을 생각해보자.
에러를 분석해 개선하는 것이 좋은 방법일 수 있다.
에러 분석에 좋은 방법 중 하나는 오차행렬을 보는 것이다.
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()
실제로 도출된 오차행렬과 이미지를 살펴보면 다음과 같다.
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 (클래스의 이미지 갯수) = 에러 비율.
이 에러 비율을 비교하자.
에러 부분에 초점 맞추기
# (오차행렬의 각 값) / (클래스의 이미지 갯수) = 에러 비율
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() 를 추가한다.
# 숫자 그림을 위한 추가 함수
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")
그리고 이미지를 획득.
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번째는 홀수인지 나타냄.
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 인스턴스를 만들어낸 후 다중 타깃 배열로 훈련
knn_clf.predict([some_digit])
>>> array([[False, True]])
- 올바르게 분류 완료!
- 5는 크지 않고(False), 홀수임(True).
다중 레이블 f1점수로 평가하기
- 여기서는 모든 레이블에 대한 F_{1} 점수의 평균을 계산
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. 다중 출력 분류
- 다중 출력 다중 레이블 이라고도 한다.
- 다중 레이블 분류에서 한 레이블이 다중 클래스가 될 수 있도록 일반화 한 것.
잡음 제거 시스템
- 이미지에 잡음 추가하기
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
- 이미지 비교
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()
-
왼쪽 : 잡음 섞인 이미지
-
오른쪽 : 깨끗한 타깃 이미지
-
분류기를 통해 깨끗하게 만든 결과
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digits(clean_digit)
- 타깃과 굉장히 비슷하다.