1. MNIST
MNIST 데이터셋 각 이미지에는 어떤 숫자를 나타내는지 레이블 되어 있다.
from sklearn.datasets import fetch_openml
# 사이킷런 1.2에서 추가된 parser 매개변수 기본값이 1.4 버전에서 'liac-arff'에서 'auto'로 바뀝니다.
# 'auto'일 경우 희소한 ARFF 포맷일 때는 'liac-arff', 그렇지 않을 때는 'pandas'가 됩니다.
# 이에 대한 경고를 피하려면 parser='auto'로 지정하세요.
mnist = fetch_openml('mnist_784', as_frame=False)
sklearn.datasets 패키지에는 세 가지 함수가 있다.
- fetch_openml() : 실전 데이터셋 다운로드
- load_* : 소규모 데이터셋 로드
- make_* : 테스트에 유용한 가짜 데이터셋 생성
X, y = mnist.data, mnist.target
X
>>> array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
X.shape
>>> (70000, 784)
y
>>> array(['5', '0', '4', ..., '4', '5', '6'], dtype=object)
y.shape
>>> (70000,)
data : 입력 데이터, 2D 넘파이 배열
target : 레이블, 1D 넘파이 배열
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)
save_fig("some_digit_plot") # 추가 코드
plt.show()
y[0]
>>> 5
데이터를 조사하기 전에 테스트 세트를 만들고 분리해야 한다.
fetch_openml()이 반환한 MNIST 데이터셋은 이미 분리가 되어있다.
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
학습 알고리즘은 순서에 민감해서 교차 검증 폴드로 훈련 세트를 섞어서 진행한다.
2. 이진 분류기 훈련
예를 들어 숫자 5를 식별해보자면,
'5-감지기'란 '5'와 '5아님' 두 개의 클래스를 구분할 수 있는 이진 분류기이다.
y_train_5 = (y_train == '5') # 5는 True고, 다른 숫자는 모두 False
y_test_5 = (y_test == '5')
이제 사이킷런의 SGDClassifier 클래스를 사용해 확률적 경사 하강법 분류기로 훈련시키겠다.
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
sgd_clf.predict([some_digit])
>>> array([ True])
분류기는 이 이미지가 5를 나타낸다고 정확히 맞췄다.
이제 성능을 평가해보겠다.
3. 성능 측정
1. 교차 검증으로 정확도 측정
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.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])
True로 예측된 것이 없기 때문에 False가 출력 된다.
5가 아닌 것으로 맞출 확률은 90% 이상이다.
이 예는 정확도는 분류기의 성능 지표로 선호하지 않는 것을 보여준다.
특히 불균형한 데이터셋을 다룰 때 그렇다. 성능을 평가하는 더 좋은 방법은 오차 행렬을 보는 것이다.
2. 오차 행렬
오차 행렬은 모든 A/B 쌍에 대해 클래스 A의 샘플이 클래스 B로 분류한 횟수를 새는 것이다.
오차 행렬을 만드려면 실제 타깃과 비교할 수 있도록 예측값을 만든다.
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_score() 함수처럼 cross_val_predict() 함수는 k-폴드 교차 검증을 수행하지만 평가 점수가 아닌 테스트 폴드에서 얻은 예측을 반환한다. 즉, 한 번도 보지 못했던 데이터에서 예측을 한 것이다.
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_train_5, y_train_pred)
cm
>>> array([[53892, 687],
[ 1891, 3530]])
y_train_perfect_predictions = y_train_5 # 완벽한 분류기일 경우
confusion_matrix(y_train_5, y_train_perfect_predictions)
>>> array([[54579, 0],
[ 0, 5421]])
오차 행렬의 행은 실제 클래스를 나타내고 열은 예측한 클래스를 나타낸다.
이 행의 첫 번째 행은 음성 클래스로 53,892개를 5가 아니라고 분류한 것이다. 나머지 687개는 5라고 잘못 분류한 것이다.
두 번째 행은 양성 클래스로 1891개를 5가 아니라고 잘못 분류했고 나머지 3530개는 5라고 분류한 것이다.
완벽한 분류기는 주대각선만 0이 아닌 값이 된다.
오차 행렬을 더 요약된 지표는 정밀도이다. 정밀도는 양성 예측의 정확도이다.
정밀도 = TP / (TP+FP) → TP는 진짜 양성의 수, FP는 거짓 양성의 수
가장 간단한 방법은 제일 확신이 높은 샘플에서 양성 예측을 하고 나머지는 음성 예측을 하는 분류기를 만드는 것이다.
이런 분류기는 모든 양성 샘플을 무시하기 때문에 유용하지 않다. 이 점을 보완한 지표는 재현율이다.
재현율 = TP / (TP+FN) → FN은 거짓 음성의 수
3. 정밀도와 재현율
사이킷런은 정밀도와 재현율을 포함해 분류기의 지표를 계산하는 함수가 많다.
from sklearn.metrics import precision_score, recall_score
precision_score(y_train_5, y_train_pred) # == 3530 / (687 + 3530)
>>> 0.8370879772350012
recall_score(y_train_5, y_train_pred) # == 3530 / (1891 + 3530)
>>> 0.6511713705958311
5-감지기는 정확도에서 봤을 만큼 정확도가 높지 않다.
정밀도와 재현율을 F1 점수라는 하나의 숫자로 만들면 비교할 때 편리하다. F1 점수는 정밀도와 재현율의 조화 평균이라고 불린다.
from sklearn.metrics import f1_score
f1_score(y_train_5, y_train_pred)
>>> 0.7325171197343846
정밀도와 재현율이 비슷한 분류기는 F1 점수가 높다. 상황에 따라 정밀도가 중요한지 재현율이 중요한지 모르기 때문에 높다고 좋은 것은 아니다. 그리고 정밀도와 재현율은 트레이드 오프 관계로 하나가 오르면 하나가 줄어드는 관계를 가지고 있다.
4. 정밀도/재현율 트레이드오프
SGDClassifier 는 결정함수를 사용하여 각 샘플의 점수를 계산한다.
점수가 임곗값보다 크면 양성 클래스에 샘플을 할당하고 그렇지 않으면 음성 클래스에 할당한다.
사이킷런에서는 임곗값을 직접 지정할 수는 없지만 예측에 사용한 점수는 확인할 수 있다.
분류기의 predict() 대신 decision_function() 메서드를 호출하면 점수를 얻을 수 있다.
y_scores = sgd_clf.decision_function([some_digit])
y_scores
>>> array([2164.22030239])
threshold = 0
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
>>> array([ True])
여기서는 임곗값이 0이므로 predict()와 같은 결과를 반환한다. 임곗값을 높여보겠다.
threshold = 3000
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
>>> array([False])
임곗값을 높이면 재현율이 줄어드는 것을 확인할 수 있다.
이미지가 실제로 숫자 5이고 임곗값이 0일 때는 이를 감지했지만 3000으로 바꾸게 되면 인지하지 못하기 때문이다.
그렇다면 적절한 임곗값은 어떻게 찾아야 할까?
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
method="decision_function")
첫 번째로 cross_val_predict() 함수로 훈련 세트의 모든 샘플의 점수를 구한다. 예측 결과가 아닌 결정 점수를 반환하도록 한다.
from sklearn.metrics import precision_recall_curve
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")
# 추가 코드 – 그림 3–5를 그리고 저장합니다
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")
save_fig("precision_recall_vs_threshold_plot")
plt.show()
이 임곗값에서 정밀도는 약 90%이고 재현율은 50%이다.
좋은 정밀도/재현율 트레이드오프를 선택하는 것은 재현율에 대한 정밀도 곡선을 그리는 것이다.
import matplotlib.patches as patches # 추가 코드 – 구부러진 화살표를 그리기 위해서
plt.figure(figsize=(6, 5)) # 추가 코드
plt.plot(recalls, precisions, linewidth=2, label="Precision/Recall curve")
# extra code – just beautifies and saves Figure 3–6
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")
save_fig("precision_vs_recall_plot")
plt.show()
재현율 80% 근처에서 정밀도가 줄어든다. 이 부분을 트레이드오프로 선택하는 것이 좋다.
idx_for_90_precision = (precisions >= 0.90).argmax()
threshold_for_90_precision = thresholds[idx_for_90_precision]
threshold_for_90_precision
>>> 3370.0194991439557
이제 정밀도를 90%에 달성하도록 해보자.
정밀도가 최소 90%가 되는 가장 낮은 임곗값을 찾을 땐 argmax() 메서드를 사용할 수 있다.
y_train_pred_90 = (y_scores >= threshold_for_90_precision)
예측을 만들 땐 predict() 대신 위 코드를 실행한다.
이 에측에 대한 정밀도와 재현율을 확인해보자.
precision_score(y_train_5, y_train_pred_90)
>>> 0.9000345901072293
recall_at_90_precision = recall_score(y_train_5, y_train_pred_90)
recall_at_90_precision
>>> 0.4799852425751706
정밀도가 90%에 달성한 분류기를 완성했다.
보다시피 임곗값을 크게 지정하면 모든 정밀도의 분류기를 만들 수 있다.
하지만 정밀도에 비해 재현율이 매우 낮다.
5. ROC 곡선
ROC 곡선도 이진 분류에 널리 사용된다. ROC 곡선은 거짓 양성 비율에 대한 진짜 양성 비율의 곡선이다.
ROC 곡선은 민감도에 대한 1-특이도 그래프이다. 그래서 곡선을 그리려면 roc_curve() 함수를 사용해 여러 임곗값에서 특이도와 민감도를 계산 해야 한다.
- 민감도 = 1 - 특이도
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)
save_fig("roc_curve_plot")
plt.show()
90% 정밀도에 해당하는 지점을 찾기 위해 임곗값의 인덱스를 찾는다.
재현율이 높을 수록 거짓 양성 비율이 늘어나는 것으로 보아 트레이드오프가 있다.
좋은 분류기는 점선에서 멀리 떨어져 있어야 한다.
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5, y_scores)
>>> 0.9604938554008616
곡선 아래의 면적 (AUC)를 이용해 분류기를 비교할 수 있다.
완벽한 분류기는 ROC의 AUC가 1이고 랜덤 분류기는 0.5이다.
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(random_state=42)
RandomForestClassifier를 만들어서 SGDClassifier의 PR곡선과 F1 점수를 비교하겠다.
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.99, 0.01]])
훈련 새트에 있는 두 개의 이미지에 대한 확률을 보면,
첫 번째 이미지를 89% 확률로 양성이라 예측하고, 두 번째 이미를 99% 확률로 음성이라 예측한다.
y_scores_forest = y_probas_forest[:, 1]
precisions_forest, recalls_forest, thresholds_forest = precision_recall_curve(
y_train_5, y_scores_forest)
두 번째 열에 양수 클래스에 대한 추정 확률이 있으므로 이를 precision_recall_curve()에 전달한다.
이제 곡선을 그려보면,
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")
save_fig("pr_curve_comparison_plot")
plt.show()
RandomForestClassifier의 PR 곡선이 SGDClassifier 곡선보다 더 좋아보인다.
점수도 계산해보면 F1 점수와 ROC-AUC 점수도 훨씬 우수하다.
y_train_pred_forest = y_probas_forest[:, 1] >= 0.5 # 양성 확률 ≥ 50%
f1_score(y_train_5, y_train_pred_forest)
>>> 0.9274509803921569
roc_auc_score(y_train_5, y_scores_forest)
>>> 0.9983436731328145
precision_score(y_train_5, y_train_pred_forest)
>>> 0.9897468089558485
recall_score(y_train_5, y_train_pred_forest)
>>> 0.8725327430363402
'ML' 카테고리의 다른 글
[Machine Learning][3] 분류 (2) (0) | 2025.03.11 |
---|---|
[Machine Laerning][2] 머신러닝 주요 단계 (3) (0) | 2025.03.07 |
[Machine Laerning][2] 머신러닝 주요 단계 (2) (0) | 2025.03.05 |
[Machine Learning][2] 머신러닝 주요 단계 (1) (0) | 2025.03.04 |
[Machine Learning][1] 머신러닝 (0) | 2025.03.03 |