from keras.src.activations import threshold
from sklearn.datasets import fetch_openml
import matplotlib.pyplot as plt
mnist = fetch_openml('mnist_784', as_frame=False)
X, y = mnist.data, mnist.target
X.shape, y.shape
((70000, 784), (70000,))
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()
y[0]
'5'
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
이진 분류기 훈련¶
y_train_5 = (y_train == '5')
y_test_5 = (y_test == '5')
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier()
sgd_clf.fit(X_train, y_train_5)
SGDClassifier()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SGDClassifier()
sgd_clf.predict([some_digit])
array([ True])
성능 측정¶
교차 검증을 사용한 정확도 측정¶
cross_val_score()함수로 폴드가 3개인 k-폴드 교차 검증을 사용해 SGD모델을 평가해보겠다.
K-폴드 교차 검증은 훈련 세트를 k개(여기서 3개)의 폴드로 나누고, 평가를 위해 매번 다른 폴드를 떼어놓고 모델을 k번 훈련한다.
from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring='accuracy')
array([0.95485, 0.95445, 0.9614 ])
정확도가 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)))
False
cross_val_score(dummy_clf, X_train, y_train_5, cv=3, scoring='accuracy')
array([0.90965, 0.90965, 0.90965])
정확도가 90% 이상이다.
이미지의 10%만 '5'이기 때문에 맞출 확률이 90%이다.
정확도를 분류기의 성능 측정 지표로 선호하지 않는다.불균형한 데이터셋을 다룰 때 (즉, 어떤 클래스가 다른 것보다 월등히 많은 경우) 더욱 그렇다.
confusion matrix을 조사하면 더 좋은 성능평가가 가능하다.
오차 행렬¶
오차 행렬은 모든 A/B쌍에 대해 클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 것이다.
예를 들어 분류기가 숫자 8의 이미지를 0으로 잘못 분류한 횟수를 알고 싶다면 오차 행렬에서 8번 행 0번 열을 보면 된다.
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
cross_val_predict() 함수는 K-폴드 교차 검증을 수행하지만 평가 점수를 반환하지 않고 각 테스트 폴드에서 얻은 예측을 반환한다.
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_train_5, y_train_pred)
cm
array([[53454, 1125], [ 1080, 4341]])
오차 행령의 행은 실제 클래스를 나타내고 열은 예측한 클래스를 나타낸다.
이 행렬의 첫 번째 행은 '5아님'(negative class)에 대한 것으로, 54,144개를 '5 아님'으로 정확하게 분류했고(true negative라고 한다.) 나머지 435개는 '5'라고 잘못 분류했다 (false positive라고 부른다)
두 번째 행은 '5' 이미지 (positive class)에 대한 것으로, 1856개를 '5 아님'으로 잘못 분류했고 (false negative라고 한다.) 나머지 3,565개를 정확히 '5'라고 분류했다 (true positive라고 한다)
precision은 **recall**이라는 지표와 함께 사용한다. 재현율은 분류기가 정확하게 감지한 양성 샘플의 비율로 **sensitivity** 또는 **true positive rate(TPR)** 이라고도 한다.
$$recall = \frac{TP}{FP + FN} \qquad (FN = false\;negative)$$
from sklearn.metrics import precision_score, recall_score
print(precision_score(y_train_5, y_train_pred))
print(recall_score(y_train_5, y_train_pred))
0.7941822173435785 0.8007747648035418
recall과 precision을 F1 score라고 하는 하나의 숫자로 만들면 편리하다.
F1 score는 harmonic mean이다.
from sklearn.metrics import f1_score
f1_score(y_train_5, y_train_pred)
0.7974648663543676
precision을 올리면 recall이 줄고 그 반대도 마찬가지다. 이를 precision/recall 트레이드오프라고 한다
y_scores = sgd_clf.decision_function([some_digit])
y_scores
array([6393.05145592])
threshold = 0
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
array([ True])
threshold = 5000
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
array([ True])
이 결과는 임계값을 높이면 recall이 줄어든다는 것을 보여준다.
실제로 이미지가 5이고 임계값이 0일 때는 분류기가 이를 감지했지만 임계값을 3,000으로 높이면 이를 놓치게 된다.
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)
임계값이 5000일때 precision, recall을 그려보자
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()
이 임계값(5000)에서는 precisiond은 90%이고 recall은 약 50%이다.
좋은 정밀도, 재현율을 선택하는 방법은 재현율에 대한 정밀도 곡선을 그리는것이다.
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()
precision을 90%로 만드는것을 목표로 해보자
idx_for_90_precision = (precisions >= 0.90).argmax()
threshold_for_90_precision = thresholds[idx_for_90_precision]
threshold_for_90_precision
np.float64(4482.791335999626)
y_train_pred_90 = (y_scores >= threshold_for_90_precision)
precision_score(y_train_5, y_train_pred_90)
0.9000563063063063
ROC 곡선¶
receiver operating characteristic(ROC) 곡선도 이진 분류에서 널리 사용되는 도구이다.
precision/recall 곡선과 매우 유사하지만, false positivie rate(FPR) 에 대한 true positivie rate(TPR) 의 곡선이다.
FPR은 양성으로 잘못 분류된 음성 샘플의 비율이다. 이는 1 - true engative rate(TNR)이다. TNR을 specificity라고도 한다.
ROC 곡선은 sensitivity에 대한 1-specificity 그래프 이다.
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
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.figure(figsize=(6, 5)) # 추가 코드
plt.plot(fpr, tpr, linewidth=2, label="ROC curve")
plt.plot([0, 1], [0, 1], 'k:', label="Random classifier's ROC curve")
plt.plot([fpr_90], [tpr_90], "ko", label="Threshold for 90% precision")
# 추가 코드 – 그림 3–7을 그리고 저장합니다
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, "Higher\nthreshold", color="#333333")
plt.xlabel('False Positive Rate (Fall-Out)')
plt.ylabel('True Positive Rate (Recall)')
plt.grid()
plt.axis([0, 1, 0, 1])
plt.legend(loc="lower right", fontsize=13)
plt.show()
area under the curve(AUC) 을 측정해 분류기들을 비교할 수 있다.
완벽한 분류기는 ROC의 AUC가 1이고, 완전한 랜덤 분류기는 0.5이다.
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5, y_scores)
np.float64(0.963917688684547)
ROC가 PR(precision/recall) 곡선과 비슷해서 어떤 것을 사용해야 할지 궁금할 수 있다.
일반적으로 양성 클래스가 드물거나 거짓 음성보다 거짓 양성이 더 중요할때 PR 곡선을 사용하고 그렇지 않다면 ROC를 사용한다.
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_probas_forest[:2]
array([[0.11, 0.89], [0.98, 0.02]])
이 모델은 첫 번째 이미지를 89%로 양성이라고 예측하고, 98% 확률로 음성이라 예측한다.
y_scores_forest = y_probas_forest[:, 1]
precisions_forest, recalls_forest, thresholds_forest = precision_recall_curve(y_train_5, y_scores_forest)
plt.figure(figsize=(6, 5)) # 추가 코드
plt.plot(recalls_forest, precisions_forest, "b-", linewidth=2,
label="Random Forest")
plt.plot(recalls, precisions, "--", linewidth=2, label="SGD")
# 추가 코드 – 그림 3–8을 그리고 저장합니다
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.axis([0, 1, 0, 1])
plt.grid()
plt.legend(loc="lower left")
plt.show()
랜덤 포레스트가 SGD보다 훨씬 좋다. PR 곡선이 오른쪽 위 모서리에 훨씬 가까워 AUC가 더 높기 때문이다.
다중 분류¶
이진 분류는 두 개의 클래스를 구별하는 반면 다중 분류기는 둘 이상의 클래스를 구별할 수 있다.
from sklearn.svm import SVC
svm_clf = SVC(random_state=42)
svm_clf.fit(X_train[:2000], y_train[:2000])
SVC(random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SVC(random_state=42)
svm_clf.predict([some_digit])
array(['5'], dtype=object)
some_digit_score = svm_clf.decision_function([some_digit])
some_digit_score.round(2)
array([[ 3.79, 0.73, 6.06, 8.3 , -0.29, 9.3 , 1.75, 2.77, 7.21, 4.82]])
svm_clf.classes_
array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], dtype=object)
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
array(['3'], dtype='<U1')
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train.astype(float)) cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3,scoring="accuracy")
오류 분석¶
가능성이 높은 모델을 찾아다고 가정하고 성능을 향상 시켜 보자.
from sklearn.metrics import ConfusionMatrixDisplay
y_tain_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
ConfusionMatrixDisplay.from_predictions(y_train, y_tain_pred, cmap="BuGn")
plt.show()
대부분 이미지가 주대각선에 있는데, 이는 이미지가 올바르게 분류되었음을 의미한다.
(5, 5)부분이 나머지보다 어두운데 이 뜻은 더 많은 오류가 발생 됬거나 데이터가 부족했을 가능성이 있다.
각 값을 해당 클래스(True label)의 총 이미지 수로 나누어(행의 합으로 나누어) 오차 행렬을 정규화하는 것이 중요하다.
ConfusionMatrixDisplay.from_predictions(y_train, y_tain_pred, normalize="true", values_format=".0%", cmap="BuGn")
plt.show()
5의 이미지는 82%만이 올바르게 분류 됐고, 10%가 8로 잘못 인식 됐다.
오류를 더 나타낼려면 올바른 예측에 대한 가중치를 0으로 설정한다.
sample_weight = (y_train_pred != y_train)
ConfusionMatrixDisplay.from_predictions(y_train, y_tain_pred, sample_weight=sample_weight, normalize="true", values_format=".0%", cmap="BuGn")
plt.show()
오차 행렬을 분석하면 분류기의 향상 방안에 관한 인사이트를 얻을 수 있다.
다중 레이블 분류¶
여러 개의 이진 꼬리표를 출력하는 분류 시스템을 multilabel classification 시스템이라고 한다.
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= '7')
y_train_odd = ((y_train.astype(int)) % 2 != 0)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
KNeighborsClassifier()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
KNeighborsClassifier()
knn_clf.predict([some_digit]) # some_digit = 5
array([[False, True]])
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.9764102655606048
label당 하나의 모델을 학습시키는것은 label간의 의존성을 포착하기 어렵다.
이 문제를 해결하기 위해 모델을 chain으로 구성할 수 있다. 한 모델이 예측을 할 때 입력 특성과 체인 앞에 있는 모델의 모든 예측을 사용한다.
from sklearn.multioutput import ClassifierChain
chain_clf = ClassifierChain(SVC(), cv=3, random_state=42)
chain_clf.fit(X_train[:2000], y_multilabel[:2000])
ClassifierChain(base_estimator=SVC(), cv=3, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
ClassifierChain(base_estimator=SVC(), cv=3, random_state=42)
SVC()
SVC()
chain_clf.predict([some_digit])
array([[0., 1.]])
다중 출력 분류¶
다중 레이블 분류에서 한 레이블이 다중 클래스가 될 수 있도록 일반화한 것이다.(즉, 값을 두 개 이상 가질 수 있다.)
np.random.seed(42)
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
plt.subplot(121); plot_digit(X_test_mod[0])
plt.subplot(122); plot_digit(y_test_mod[0])
plt.show()
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[0]])
plot_digit(clean_digit)
plt.show()
이 내용은 [Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow]책을 참조하고 있습니다.¶
'Data science > 머신러닝' 카테고리의 다른 글
housing 데이터를 이용한 머신러닝 학습 (0) | 2025.03.28 |
---|---|
Machine Learning란? (0) | 2025.03.16 |
PCA(주성분 분석) (3) | 2022.07.23 |
K-means clustering(k-평균 알고리즘) (0) | 2022.07.23 |
Loss function(손실 함수) (0) | 2022.07.17 |