[머신러닝] 분류: MNIST 데이터셋 실습 - 1
분류란?
MINIST
◆ MINIST 데이터셋이란? (출처: 위키백과)
MNIST 데이터베이스 (Modified National Institute of Standards and Technology database)는 손으로 쓴 숫자들로 이루어진 대형 데이터베이스이며, 다양한 화상 처리 시스템을 트레이닝하기 위해 일반적으로 사용된다. 이 데이터베이스는 또한 기계 학습 분야의 트레이닝 및 테스트에 널리 사용된다. NIST의 오리지널 데이터셋의 샘플을 재혼합하여 만들어졌다. 개발자들은 NIST의 트레이닝 데이터셋이 미국의 인구조사국 직원들로부터 취합한 이후로 테스팅 데이터셋이 미국의 중등학교 학생들로부터 취합되는 중에 기계 학습 실험에 딱 적합하지는 않은 것을 느꼈다. 게다가 NIST의 흑백 그림들은 28x28 픽셀의 바운딩 박스와 앤티엘리어싱 처리되어 그레이스케일 레벨이 들어가 있도록 평준화되었다.
MNIST 데이터베이스는 60,000개의 트레이닝 이미지와 10,000개의 테스트 이미지를 포함한다. 트레이닝 세트의 절반과 테스트 세트의 절반은 NIST의 트레이닝 데이터셋에서 취합하였으며, 그 밖의 트레이닝 세트의 절반과 테스트 세트의 절반은 NIST의 테스트 데이터셋으로부터 취합되었다
◆ MNIST 데이터셋 다운 받기
사이킷런에서 제공하는 여러 헬퍼 함수를 사용해 잘 알려진 데이터셋을 내려받을 수 있으며 MNIST 역시 그 중 하나이다.
# OpenML에서 MNIST 다운 받기
from sklearn.datasets import fetch_openml
# fetch_openml() 함수는 기본적으로 입력을 판다스 데이터 프레임, 레이블을 판다스 시리즈로 반환
# MNIST 데이터셋은 이미지이므로 데이터 프레임 맞지 않으므로 as_frame=False
mnist = fetch_openml('mnist_784', as_frame=False)
# 배열 확인
X, y = mnist.data, mnist.target
print(X.shape)
print(X)
print(y.shape)
print(y)
▶ 이미지가 70,000개 있고 각 이미지에는 784개의 특성이 있다.
[데이터셋에서 이미지 하나 확인해보기]
샘플의 특성 벡터를 추출해서 28*28 배열로 크기를 바꾸고 맷플롯립의 imshow() 함수를 사용해 그리는데, cmap='binary'로 지정해 0을 흰색, 255를 검은색으로 나타내는 흑백 컬러맵을 사용한다.
import matplotlib.pyplot as plt
def plot_digit(image_data):
image = image_data.reshape(28, 28)
plt.imshow(image, cmap='binary')
plt.axis('off')
some_digit = X[0]
plot_digit(some_digit)
plt.show()
해당 그림은 숫자 5로 보이는데, 실제로 맞는지 확인
y[0]
★ 데이터를 조사하기 전에 항상 테스트 세트를 만들고 따로 떼어놓아야 한다.
fetch_openml()이 반환한 MNIST 데이터셋의경우 이미 훈련 세트(앞 60,00개 이미지) 와 테스트 세트(뒤쪽 10,000개 이미지)로 나눠어 있다.
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
이진 분류기 훈련
숫자 5만 식별하는, 두 개의 클래스를 구분할 수 있는 이진 분류기를 만들어본다.
y_train_5 = (y_train == '5') # 5는 True이고, 다른 숫자는 모두 False
y_test_5 = (y_test == '5')
분류 모델로 사이킷런의 SGDClassifier 확률적 경사 하강법(SGD)을 선택해서 훈련시켜본다.
확률적 경사 하강법 분류기의 장점은 매우 큰 데이터셋을 효율적으로 처리할 수 있다는 점이며, SGD는 한 번에 하나씩 훈련 샘플을 독립적으로 처리할 수 있어 온라인 학습에 잘 들어맞는다.
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
# 이 모델을 이용해 숫자 5 이미지 감지
sgd_clf.predict([some_digit])
▶ 분류기는 이 이미지가 5를 나타낸다(True)고 추측했으니 이 샘플에 대해서는 정확히 맞춘 것으로 보인다.
성능 측정
분류기를 평가하는 방법으로는 성능 지표를 사용하는 것인데, 대략적인 성능 지표들은 다음과 같다.
- 교차 검증을 사용한 정확도 측정: cross_val_score() 함수
- 오차 행렬: sklearn.metrics의 confusion_matrix
- 정밀도와 재현율: sklearn.metrics의 precision_score, recall_score
- 정밀도/재현율 트레이드오프: 재현율에 대한 정밀도 곡선을 그려 좋은 정밀도/재현율 트레이드오프 선택. sklearn.metrics 의 precision_recall_curve
- ROC 곡선: 거짓 양성 비율(FPR)에 대한 진짜 양성 비율(TPR 또는 재현율)의 곡선 ▶민감도(재현율)에 대한 1-특이도(진짜 음성율, TNR) 그래프. sklearn.metrics의 roc_curve
- AUC: ROC 곡선 아래의 면적. 완벽한 분류기는 ROC의 AUC가 1, 완전한 랜덤 분류기는 0.5. sklearn.metrics의 roc_auc_curve
1) 교차 검증을 사용한 정확도 측정
cross_val_score() 함수로 폴드가 3개인 k-폴드 교차 검증을 사용해 SGDClassifier 모델 평가(정확도, accuracy)
from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
▶ 모든 교차 검증 폴드에 대한 정확도가 95% 이상으로 나왔다.
이번에는 모든 이미지를 가장 많이 등장하는 클래스 ('5 아님')로 분류하는 더미 분류기를 만들어 정확도를 비교해 본다.
from sklearn.dummy import DummyClassifier
dummy_clf = DummyClassifier()
dummy_clf.fit(X_train, y_train_5)
print(any(dummy_clf.predict(X_train)))
True로 예측된 것이 없으므로 False 출력
cross_val_score(dummy_clf, X_train, y_train_5, cv=3, scoring="accuracy")
▶ 정확도가 90% 이상으로 나왔는데, 이미지의 10% 정도만 숫자 5이기 때문에 무조건 '5아님'으로 예측하면 정확히 맞출 확률이 90%라는 의미이다.
이 때문에 불균형한 데이터셋을 다룰 때 정확도를 분류기의 성능 측정 지표로 선호하지 않으며, 이때에는 오차 행렬(Confusion Matrix)을 조사하는 게 더 좋은 방법이다.
2) 오차 행렬
오차 행렬을 만들려면 실제 타깃과 비교할 수 있도록 예측값을 만들어야 하는데, 테스트 세트를 사용할 수 없으므로 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)
이제 confusion_matrix() 함수를 사용해 오차 행렬을 만든다.
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_train_5, y_train_pred)
cm
▶ 행렬의 첫 번째 행은 '5아님' 이미지에 대한 것으로, 53892개를 '5아님'으로 정확히 분류하고, 687개는 '5'라고 잘못 분류.
두 번째 행은 '5' 이미지에 대한 것으로 1,891개를 '5아님'으로 잘못 분류했고, 3530개를 정확히 '5'라고 분류 했다.
분류 | 예측 클래스 | 예측 클래스 |
실제 클래스 (음성) : '5'아님 이미지 | TN (실제 음성) : 실제 '5 아님' | FP(거짓 양성): '5'라고 잘못 분류 <1종 오류> |
실제 클래스 (양성): '5' 이미지 | FN(거짓 음성): '5아님' 이라고 잘못 분류 <2종 오류> |
TP (실제 양성): 실제 '5' |
완벽한 분류기라면 진짜 양성과 진짜 음성만 가지고 있을 것이므로 오차 행렬의 주대각선만 0이 아닌 값이 된다.
y_train_perfect_predictions = y_train_5 #완벽한 분류기일 경우
confusion_matrix(y_train_5, y_train_perfect_predictions)
3) 정밀도와 재현율, F1-Score
from sklearn.metrics import precision_score, recall_score
print('정밀도:', round(precision_score(y_train_5, y_train_pred),2))
print('재현율:', round(recall_score(y_train_5, y_train_pred),2))
정밀도와 재현율을 F1-Socre 라고 하는 하나의 숫자로 만들면 편리할 때가 많은데, F1-Score는 정밀도와 재현율의 조화 평균이며, 조화 평균은 낮은 값에 훨씬 더 높은 비중을 둔다.
F1-Score를 계산하려면 f1_score() 함수를 호출하면 된다.
from sklearn.metrics import f1_score
print('F1-Score:', round(f1_score(y_train_5, y_train_pred),2))
결과적으로 분류기의 F1-Score가 높아지려면 재현율과 정밀도가 모두 높아야 하는데, 모두 얻을 수는 없다.
정밀도를 올리면 재현율이 줄고 그 반대 역시 마찬가지인데, 이를 정밀도/재현율 트레이드 오프라고 한다.
4) 정밀도/재현율 트레이드오프
임곗값이 높을수록 재현율은 낮아지고 정밀도는 높아지며, 임곗값을 내리면 재현율은 높아지고 정밀도가 낮아진다.
사이킷런에서 임곗값을 직접 지정할 수는 없지만 예측에 사용한 점수는 확인할 수 있는데, 분류기의 predict() 메서드 대신 decision_function() 메서드를 호출하면 각 샘플의 점수를 얻을 수 있다.
이 점수를 기반으로 원하는 임곗값을 정해 예측을 만들 수 있다.
y_scores = sgd_clf.decision_function([some_digit])
print(y_scores)
threshold = 0
y_some_digit_pred = (y_scores > threshold)
print(y_some_digit_pred)
임곗값을 높이면 재현율이 줄어든다는 것을 보여준다.
threshold = 3000
y_some_digit_pred = (y_scores > threshold)
print(y_some_digit_pred)
이미지가 실제로 숫자 5이고 임곗값이 0일 때는 분류기가 이를 감지했지만 임곗값을 3000으로 높이면 이를 놓치게 된다.
적절한 임곗값을 정하기 위해서는 cross_val_predict() 함수를 사용해 훈련 세트에 있는 모든 샘플의 점수를 구하고 결정 점수를 반환하도록 지정해야 한다.
그리고 이 점수로 precision_recall_curve() 함수를 사용하여 가능한 모든 임곗값에 대해 정밀도와 재현율을 계산할 수 있다.
from sklearn.metrics import precision_recall_curve
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
method="decision_function")
# 모든 임곗값에 대한 정밀도와 재현율 계산
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
# 임곗값의 함수로 정밀도와 재현율 그래프 그리기
plt.figure(figsize=(8, 4))
plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)
plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)
plt.vlines(threshold, 0, 1.0, "k", "dotted", label="threshold")
# 그리드, 범례, 축, 레이블 등 추가
idx = (thresholds >= threshold).argmax() # 첫 번째 index ≥ threshold
plt.plot(thresholds[idx], precisions[idx], "bo")
plt.plot(thresholds[idx], recalls[idx], "go")
plt.axis([-50000, 50000, 0, 1])
plt.grid()
plt.xlabel("Threshold")
plt.legend(loc="center right")
plt.show()
▶ 정밀도 곡선이 재현율 곡선보다 더 울퉁불퉁한 이유는 임곗값을 올리더라도 정밀도가 낮아질 때가 가끔 있기 때문이다.
좋은 정밀도/재현율 트레이드오프를 선택하는 다른 방법은 재현율에 대한 정밀도 곡선을 그리는 것이다.
import matplotlib.patches as patches
plt.figure(figsize=(6, 5))
plt.plot(recalls, precisions, linewidth=2, label="Precision/Recall curve")
plt.plot([recalls[idx], recalls[idx]], [0., precisions[idx]], "k:")
plt.plot([0.0, recalls[idx]], [precisions[idx], precisions[idx]], "k:")
plt.plot([recalls[idx]], [precisions[idx]], "ko",
label="Point at threshold 3,000")
plt.gca().add_patch(patches.FancyArrowPatch(
(0.79, 0.60), (0.61, 0.78),
connectionstyle="arc3,rad=.2",
arrowstyle="Simple, tail_width=1.5, head_width=8, head_length=10",
color="#444444"))
plt.text(0.56, 0.62, "Higher\nthreshold", color="#333333")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.axis([0, 1, 0, 1])
plt.grid()
plt.legend(loc="lower left")
plt.show()
▶ 재현율 80%근처에서 정밀도가 급격하게 줄어들기 시작하며, 이 하강점 직전을 정밀도/재현율 트레이드오프로 선택하는 것이 좋다.
만약 정밀도 90%를 달성하는 것이 목표라고 할 경우 정밀도가 최소 90%가 되는 가장 낮은 임곗값을 찾는 것이 좋은 방법이 될 수 있다. 이를 위해 넘파이 배열의 argmax() 메서드를 사용하면 되는데, 이 메서드는 최댓값의 첫 번째 인덱스를 반환한다.
idx_for_90_precision = (precisions >= 0.90).argmax()
threshold_for_90_precision = thresholds[idx_for_90_precision]
threshold_for_90_precision
훈련 세트에 대한 예측을 만들려면 분류기의 predict() 메서드를 호출하는 대신 아래의 코드를 실행한다
y_train_pred_90 = (y_scores >= threshold_for_90_precision)
# 이 예측에 대한 정밀도와 재현율 확인
precision_score(y_train_5, y_train_pred_90)
recall_at_90_precision = recall_score(y_train_5, y_train_pred_90)
recall_at_90_precision
▶ 정밀도 90%를 달성한 분류기를 만들었으나, 재현율 48%는 훌륭한 값이 아니기 때문에 유용하지 않다.
그러므로, 정밀도를 달성할 때에는 재현율 수치 역시 감안을 해야 한다.
5) ROC 곡선, AUC
ROC 곡선은 roc_curve() 함수를 사용해 여러 임곗값에서 TPR과 FPRT을 계산해야 한다. 그리고 맷플롯립을 사용해 TPR에 대한 FPR 곡선을 나타낼 수 있다.
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
# ROC 곡선 그리기
idx_for_threshold_at_90 = (thresholds <= threshold_for_90_precision).argmax()
tpr_90, fpr_90 = tpr[idx_for_threshold_at_90], fpr[idx_for_threshold_at_90]
plt.plot(fpr, tpr, linewidth=2, label='ROC 곡선')
plt.plot([0, 1], [0, 1], 'k:', label='랜덤 분류기의 ROC 곡선')
plt.plot([fpr_90], [tpr_90], "ko", label='90% 정밀도에 대한 임곗값')
plt.gca().add_patch(patches.FancyArrowPatch(
(0.20, 0.89), (0.07, 0.70),
connectionstyle="arc3,rad=.4",
arrowstyle="Simple, tail_width=1.5, head_width=8, head_length=10",
color="#444444"))
plt.text(0.12, 0.71, "임곗값 증가 방향", color="#333333")
plt.xlabel('거짓 양성 비율 (Fall-Out)')
plt.ylabel('진짜 양성 비율 (Recall)')
plt.grid()
plt.axis([0, 1, 0, 1])
plt.legend(loc="lower right", fontsize=13)
plt.show()
▶ 여기에도 트레이드오프가 있는데, 재현율이 높을수록 분류기가 만드는 거짓 양성 비율이 늘어난다. 점섬은 완전한 분류기의 ROC 곡선을 뜻하는데, 좋은 분류기는 이 점선에서 최대한 멀리 떨어져 있어야 한다.
AUC(곡선 아래의 면적)을 측정해 분류기들을 비교할 수 있다.
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5, y_scores)
RandomForestClassifier 를 만들어 SGDClassifier의 PR 곡선과 F1-Score를 비교해 본다
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(random_state=42)
RandomForestClassifier 는 작동 방식 때문에 decision_function()을 제공하지 않는 대신 각 샘플에 대한 클래스 확률을 반환하는 predict_proba() 메서드를 제공하며, 이 중 양성 클래스에 대한 확률을 점수로 사용할 수 있다.
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3,
method="predict_proba")
훈련 세트에 있는 처음 두 개의 이미지에 대한 클래스 확률을 확인해 본다
y_probas_forest[:2]
두 번째 열에 양수 클래스에 대한 추정 확률이 포함되어 있으므로 이를 precision_recall_curve() 함수에 전달한다
y_scores_forest = y_probas_forest[:, 1]
precisions_forest, recalls_forest, thresholds_forest = precision_recall_curve(
y_train_5, y_scores_forest)
이제 PR 곡선을 그려본다
plt.figure(figsize=(6, 5))
plt.plot(recalls_forest, precisions_forest, "b-", linewidth=2,
label="랜덤 포레스트")
plt.plot(recalls, precisions, "--", linewidth=2, label="SGD")
plt.xlabel("재현율")
plt.ylabel("정밀도")
plt.axis([0, 1, 0, 1])
plt.grid()
plt.legend(loc="lower left")
plt.show()
▶ 랜덤 포레스트의 PR 곡선이 오른쪽 위 모서리에 훨씬 가까워 AUC가 높기 때문에 랜덤 포레스트 분류기가 SGD 분류기보다 훨씬 좋다
F1-Score와 AUC 점수, 그리고 정밀도와 재현율 점수도 확인해 본다
y_train_pred_forest = y_probas_forest[:, 1] >= 0.5 # 양성 확률 ≥ 50%
print('F1-Score:', round(f1_score(y_train_5, y_train_pred_forest), 4))
print('ROC_AUC Score:', round(roc_auc_score(y_train_5, y_scores_forest), 4))
print('Precision Score:', round(precision_score(y_train_5, y_train_pred_forest), 4))
print('Recall Score:', round(recall_score(y_train_5, y_train_pred_forest), 4))
다음 내용
[출처]
핸즈온 머신러닝
위키백과