데이터셋 확인
사용할 데이터셋 : MNIST 데이터셋 (70,000개의 작은 숫자 이미지, 각 이미지마다 숫자 레이블 존재)
sklearn.datasets 패키지의 함수 3가지 종류
1) fetch_*() : fetch_openml()과 같이 실전 데이터셋을 다운로드하기 위한 함수. fetch_openml() 함수는 기본적으로 입력을 pandas dataframe, 레이블을 pandas series로 반환.
2) load_*() : scikit_learn에 번들로 포함된 소규모 데이터셋을 로드하기 위한 함수(다운로드 불필요)
3) make_*() : 테스트에 유용한 가짜 데이터셋을 생성하기 위한 함수
→ 생성된 데이터셋은 일반적으로 numpy 배열이고 (X,y) 튜플로 반환된다.
sklearn.utils.Bunch 객체로 반환되는 데이터셋도 있다. 객체는 DESCR, data(입력), target(레이블) 참조 가능하다.
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', as_frame=False) # MNIST 데이터셋은 이미지미으로 as_frame=False로 지정해 numpy 배열로 데이터 받기
# as_frame의 매개변수 기본값은 auto, 데이터셋이 희소 행렬로 저장되어 있지 않다면 dataframe(입력)과 series(레이블) 반환.
X, y = mnist.data, mnist.target
X.shape # (70000,784)
y.shape # (70000,)
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
(참고) 훈련 샘플의 순서를 고려하여 데이터셋을 섞으면 좋지 않은 경우 : 시계열 데이터
샘플을 섞어야 하는 대표적인 경우 : SGD (max_iter epoch마다 훈련 데이터를 다시 섞는다.)
# 이미지 확인
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()
Binary Classifier 훈련 (SGDClassifier)
Scikit-learn의 SGDClassifier 클래스로 SGD(확률적 경사 하강법) 분류기 사용.
매우 큰 데이터셋을 효율적으로 처리할 수 있다. 한 번에 하나씩 훈련 샘플을 독립적으로 처리할 수 있다.
# 타깃 벡터 만들기
y_train_5 = (y_train == '5') # 5는 True고, 다른 숫자는 모두 False
y_test_5 = (y_test == '5')
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
성능 측정
교차 검증을 사용한 정확도 측정
sklearn.model_selection 패키지의 cross_val_score() 함수로 fold가 3개인 k-fold cross validation 사용해 accuracy 측정.
(훈련 세트를 k개의 fold로 나누고, 평가를 위해 매번 다른 fold를 분리시켜 모델을 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.95035, 0.96035, 0.9604 ])
# 모든 이미지를 가장 많이 등장하는 클래스(5가 아닌 경우)로 분류하는 더미 분류기 만들어 비교
from sklearn.dummy import DummyClassifier
dummy_clf = DummyClassifier()
dummy_clf.fit(X_train, y_train_5)
print(any(dummy_clf.predict(X_train)))
cross_val_score(dummy_clf, X_train, y_train_5, cv=3, scoring="accuracy")
# array([0.90965, 0.90965, 0.90965])
▶ 불균형한 데이터셋을 다룰 때 Accuracy는 성능 측정 지표로 선호되지 않는다.
오차 행렬
기본 아이디어 : 모든 A/B 쌍에 대해 클래스 A의 샘플이 클래스 B로 분류된 횟수를 count한다. (Column : 실제 class / Row : 예측 class)
먼저 cross_val_predict() 함수를 사용하여 실제 타깃과 비교할 수 있도록 예측값을 만들어야 한다.
→ k-fold cross validation을 수행하지만 평가 점수를 반환하지 않고 각 테스트 fold에서 얻은 예측을 반환한다.
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
cm = confusion_matrix(y_train_5, y_train_pred)
cm
완벽한 분류기라면 TP, TN만 가지고 있을 것이므로 오차 행렬의 주대각선만 0이 아닌 값이 된다.
y_train_perfect_predictions = y_train_5 # 완벽한 분류기일 경우
confusion_matrix(y_train_5, y_train_perfect_predictions)
- 정밀도(Precision) : TP의 정확도
- 재현율(Recall) : 분류기가 정확하게 감지한 양성 샘플의 비율. Sensitivity 또는 TPR(True Positive Rate)라고도 한다.
- F1 Score : 정밀도와 재현율의 조화 평균
from sklearn.metrics import precision_score, recall_score, f1_score
precision_score(y_train_5, y_train_pred) # cm[1, 1] / (cm[0, 1] + cm[1, 1])
recall_score(y_train_5, y_train_pred) # cm[1, 1] / (cm[1, 0] + cm[1, 1])
f1_score(y_train_5, y_train_pred) # cm[1, 1] / (cm[1, 1] + (cm[1, 0] + cm[0, 1]) / 2)
▶ 정밀도/재현율 trade-off
정밀도와 재현율이 비슷한 분류기에서는 F1 Score가 높지만, 항상 바람직한 것은 아니다. → 상황에 따라 정밀도와 재현율의 중요성을 판단하자!
SGDClassifier : 결정 함수를 사용하여 각 샘플의 점수를 계산한다. 점수가 임곗값보다 크면 샘플을 양성 클래스에 할당한다.
적절한 임곗값을 설정하려면?
→ cross_val_predict() 함수로 훈련 세트에 있는 모든 샘플의 score 구해야 한다. 이 때,
예측 결과
가 아닌 결정 점수 반환하도록 지정해야 한다! (decision_function 메서드)
→ 구한 score로 precision_recall_curve() 함수를 사용하여 가능한 모든 임곗값에 대해 정밀도와 재현율을 계산한다. 각 샘플에 대한 레이블과 점수를 기대한다.
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import precision_recall_curve
y_scores = sgd_clf.decision_function([some_digit])
# 결정 임곗값 선택 - 임곗값이 높을수록 재현율은 낮아지고 정밀도는 높아진다.
threshold = 0
y_some_digit_pred = (y_scores > threshold) # y_scores > 0은 predict() 호출과 같은 효과
# 적절한 임곗값 설정
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.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")
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")
또 다른 방법으로, 재현율에 대한 정밀도 곡선을 그려볼 수 있다. 정밀도가 급격하게 줄어들기 시작하는 하강점 직전을 정밀도/재현율 트레이드오프로 선택하는 것이 좋다.
Scikit-learn의 PrecisionRecallDisplay
클래스를 사용해 그릴 수도 있다.
정밀도 90% 달성이 목표라면, 정밀도가 최소 90%가 되는 가장 낮은 임곗값을 찾는 것이 다른 방법이 될 수 있다.
→ 정밀도뿐만 아니라 “얼마의 재현율”에서의 목표인지도 따져봐야한다!
idx_for_90_precision = (precisions >= 0.90).argmax() # numpy 배열의 argmax() 메서드 : 최댓값의 첫번째 인덱스 반환.
threshold_for_90_precision = thresholds[idx_for_90_precision]
# 훈련 세트에 대한 예측 만들기
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)
ROC 곡선
ROC(Receiver Operating Characteristic) 곡선 : FPR(False Positive Rate)에 대한 TPR(재현율,Sensitivity)의 비율에 대한 곡선으로, 재현율에 대한 1 - Specificity의 그래프.
- FPR(fall-out) = 양성으로 잘못 분류된 음성 샘플의 비율 = 1 - TNR(Specificity)
- 재현율이 높을수록 분류기가 만드는 FPR이 늘어난다.
- Scikit-learn의
RocCurveDisplay
클래스를 사용해 그릴 수도 있다.
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.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")
곡선 아래의 면적(AUC)을 측정해 분류기들을 비교할 수 있다. 완벽한 분류기는 ROC의 AUC가 1이고, 완전한 랜덤 분류기는 0.5이다.
(참고) 랜덤 분류기 : 훈련 데이터의 클래스 비율을 따라 랜덤으로 예측한다.
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5, y_scores)
예제 (RandomForestClassifier)
F1 Score 비교 : RandomForestClassifier v/s SGD Classifier의 PR 곡선
RandomForestClassifier은 작동 방식 때문에 decision_function()을 제공하지 않으므로 각 샘플에 대한 클래스 확률을 반환하는 predict_proba() 메서드를 통해 각 샘플에 score을 구해야한다.
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] # 훈련 세트에 있는 처음 2개의 이미지에 대한 클래스 확류 확인
# array([[0.11, 0.89],
# [0.99, 0.01]])
# 위의 클래스 확률은 '추정 확률'이다. sklearn.calibration 패키지를 통해 추정 확률을 보정하여 실제 확률에 훨씬 가깝게 만들 수 있다.
y_scores_forest = y_probas_forest[:, 1]
precisions_forest, recalls_forest, thresholds_forest = precision_recall_curve(
y_train_5, y_scores_forest)
# PR 곡선 그려서 비교해보기
plt.plot(recalls_forest, precisions_forest, "b-", linewidth=2,
label="Random Forest")
plt.plot(recalls, precisions, "--", linewidth=2, label="SGD")
# F1 Score과 AUC score 확인
y_train_pred_forest = y_probas_forest[:, 1] >= 0.5 # 양성 확률 ≥ 50% (이미 추정 확률을 가지고 있으므로 더 빠른 비교 가능)
f1_score(y_train_5, y_train_pred_forest)
roc_auc_score(y_train_5, y_scores_forest)
Multiclass Classification
여러 개의 클래스를 직접 처리 할 수 있는 알고리즘 : LogisticRegression, RandomForestClassifier, GaussianNB
이진 분류만 가능한 알고리즘 : SGDClassifier, SVC
▶ 이진 분류기를 여러 개 사용해 다중 클래스를 분류하는 기법도 많다.
- OvR(one-versus-the-rest) 전략 또는 OvA(one-versus-all) 전략 : 특정 숫자 하나만 구분하는 숫자별 이진 분류기를 n개 훈련시켜 클래스가 n개인 숫자 이미지 분류 시스템을 만든다. 이후, 이미지를 분류할 때 각 분류기의 결정 점수 중에서 가장 높은 것을 클래스로 선택한다.
- 대부분의 이진 분류 알고리즘에서 선호된다.
- OvO(one-versus-one) 전략 : 각 숫자의 조합마다 이진 분류기를 훈련시킨 후 가장 많이 positive으로 분류된 클래스를 선택한다. (0과 1 구별, 0과 2 구별….) 훈련 횟수는 $nC_2$이다.
- 장점 : 각 분류기의 훈련에 전체 훈련 세트 중 구별할 두 클래스에 해당하는 샘플만 있으면 된다.
- 예시 : SVM은 훈련 세트의 크기에 민감해서 큰 훈련 세트에서 몇 개의 분류기를 훈련시키는 것보다 작은 훈련 세트에서 많은 분류기를 훈련시키는 쪽이 빠르다.
다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면, Sckit-learn이 알고리즘에 따라 자동으로 OvR 또는 OvO를 실행한다.
만약 OvO나 OvR을 사용하도록 강제하고 싶다면, sklearn.multiclass
패키지의 OneVsOneClassifier나 OneVsRestClassifier를 사용해 이진 분류기 인스턴스를 만들어 객체 생성 시 전달한다.
ex-1) sklearn.svm.SVC 클래스를 사용해 SVM Classifier 테스트 - SVC는 항상 OvO 전략을 자동으로 선택한다.
from sklearn.svm import SVC
svm_clf = SVC(random_state=42)
svm_clf.fit(X_train[:2000], y_train[:2000]) # y_train_5가 아니고 y_train을 사용
svm_clf.predict([some_digit])
some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores.round(2)
# array([[ 3.79, 0.73, 6.06, 8.3 , -0.29, 9.3 , 1.75, 2.77, 7.21,
# 4.82]])
# 실제로 샘플마다 총 10개의 점수를 반환
# 클래스 레이블 확인
svm_clf.classes_
# array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], dtype=object)
svm_clf.classes_[class_id]
ex-2) OvO나 OvR 사용 강제 - SVC 기반
from sklearn.multiclass import OneVsRestClassifier
ovr_clf = OneVsRestClassifier(SVC(random_state=42))
ovr_clf.fit(X_train[:2000], y_train[:2000])
ovr_clf.predict([some_digit])
len(ovr_clf.estimators_) # 10
ex-3) OvO나 OvR 사용 강제 - SGD 기반
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
오류 분석
오차 행렬을 컬러 그래프로 나타내면 생성된 오류의 종류를 더욱 쉽게 분석할 수 있다.
▶ Data augmentation : 훈련 이미지를 약간 이동시키거나 회전된 변형 이미지로 훈련 집합을 보강하는 방법. 이미지의 위치나 회전 방향에 민감한 분류기가 변형에 더 잘 견디도록 학습시킬 수 있다.
from sklearn.metrics import ConfusionMatrixDisplay
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
sample_weight = (y_train_pred != y_train) # 올바른 예측에 대한 가중치를 0으로 설정 -> 어떤 종류의 오류인지 더 명확하게 확인 가능
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred,
sample_weight=sample_weight,
normalize="true", values_format=".0%")
# normalize를 true로 지정하면 각 값을 해당 클래스(true 레이블)의 총 이미지 수로 나누어 오차 행렬을 정규화할 수 있다.(행 단위)
# pred로 지정하면 열 단위로 정규화할 수 있다.
# values_format을 ',0%'로 지정하면 소수점 없이 백분율을 표시할 수 있다.
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('int8') % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd] # 2개의 타깃 레이블
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
knn_clf.predict([some_digit])
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") # 모든 레이블의 가중치가 같다고 가정
# average="weighted"로 설정한다면, 레이블에 클래스의 support(타깃 레이블에 속한 샘플 수)를 가중치로 줄 수 있다.
Decision tree, RandomForest, OneVsRestClassifier에서도 다중 레이블 분류를 지원한다.
SVC와 같이 다중 레이블 분류를 지원하지 않는 분류기를 사용하는 경우, 레이블당 하나의 모델을 학습시켜볼 수 있다.
그러나 레이블 간의 의존성을 포착하기 어려울 수 있다.
▶ 모델을 "Chain으로 구성하자! 한 모델이 예측을 할 때 입력 특성과 chain 앞에 있는 모델의 모든 예측을 사용한다.
sklearn.multioutput 패키지의 ClassifierChain 클래스 : 훈련에 진짜 레이블을 사용하며 체인 내 위치에 따라 각 모델에 적절한 레이블을 공급한다.
from sklearn.multioutput import ClassifierChain
chain_clf = ClassifierChain(SVC(), cv=3, random_state=42)
chain_clf.fit(X_train[:2000], y_multilabel[:2000])
chain_clf.predict([some_digit])
Multioutput Classification
다중 레이블 분류에서 한 레이블이 다중 클래스가 될 수 있도록 일반화한 것이다.
(ex) 잡음이 많은 숫자 이미지를 입력으로 받아 깨끗한 숫자 이미지를 MNIST 이미지처럼 픽셀의 강도를 담은 배열로 출력한다.
분류기의 출력 : 다중 레이블, 각 레이블은 값을 여러 개 가진다.(0-255 픽셀 강도)
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
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()
'Book Review > 핸즈온 머신러닝 3판' 카테고리의 다른 글
4장 - 모델 훈련 (0) | 2024.04.13 |
---|---|
2장 - 머신러닝 프로젝트 처음부터 끝까지 (0) | 2024.02.29 |
1장 - 한눈에 보는 머신러닝 (2) | 2024.02.10 |