[핸즈온 머신러닝] 3. 분류

박경민·2023년 3월 15일
0

MNIST

MNIST 는 고등학생과 미국 인구조사국 직원들이 손으로 쓴 70000개의 작은 숫자 이미지다. 이미지에는 어떤 숫자들을 나타내는지 레이블이 되어있다. (머신러닝 분야의 "Hello World" 라고 한다.)

다음은 MNIST 를 내려받는 코드.

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.keys()

사이킷런의 데이터들이 일반적으로 가지고 있는 딕셔너리 구조는

  • DESCR : 묘사하다로 데이터셋을 설명하는 키이다.
  • data : 샘플이 하나의 행, 특성이 하나의 열로 구성된 배열이다.
  • target : 레이블 배열을 담은 키이다.

각각 X, y 에 담아서 이 배열들을 살펴보자.

X, y = mnist["data"], mnist["target"]
X.shape
y.shape


data 는 7만 * 784, target 은 7만 건의 데이터가 존재한다.

7만건은 각 이미지이며 특성의 개수가 784개라는 의미이다. 이는 이미지가 28 * 28 픽셀이기 때문이다! 각 특성 하나는 0부터 255 (흰 ~검) 의 값을 지니며, 이는 픽셀 강도를 나타낸다. 이미지 하나를 확인하는 코드.

%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt

some_digit = X[0] # 첫번째 데이터 추출.
some_digit_image = some_digit.reshape(28, 28) # 하나의 일련의 배열을 reshape(행, 열) 로 배치할 수 있다.
plt.imshow(some_digit_image, cmap=mpl.cm.binary) #시각화
plt.axis("off")

save_fig("some_digit_plot")
plt.show()

5처럼 보이는 이 그림을 실제 이는 X[0] 의 배열을 28 * 28 로 나타내어 찍어본 것인데 y[0] 로 레이블을 확인하면 5가 나온다.

그러나 위와 같이 문자로 출력되고 있으므로 y의 값들을 숫자로 바꿔주자.

y = y.astype(np.uint8) #재정의로 타입 변경. 해당 데이터 프레임의 타입이 모두 같아야 변경 가능. 

astype() 에 관해서라면 다음 블로그를 참고하도록 하자.

MNIST 데이터셋은 훈련세트와 테스트세트를 이미 순서대로 정리해놨으므로 슬라이싱해서 떼어놓기만 하면 된다. (앞 6만개가 훈련, 뒤에 만개가 테스트)

X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

3.2 이진 분류기 훈련

문제를 단순화하여 받아온 숫자가 5인지 5가 아닌지만 분류하는 5-이진분류기를 만들어보자.

y_train_5 = (y_train == 5) # 5이면 true 를 반환하는 변수
y_test_5 = (y_test == 5)

분류모델을 하나 선택하여 훈련해보자.

이번에 사용할 분류 모델은 확률적 경사하강법 (SGD) 분류기로 사이킷런에서 SGDClassifier 를 임포트해준다. 확률적 경사하강법에 대해 알고싶다면 이전에 작성한 글을 참고하자. 일반적인 경사하강법과 다르니 꼭 주의하자.

모델을 만들고 훈련시키자.

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])

some_digit 은 아까 추출했던 X[0] 이므로 실제로 라벨링값인 True 와 일치하는 예측값을 얻었다! 모델의 성능은 어떨까?

3.3 성능 측정

3.3.1 교차 검증을 사용한 정확도 측정

폴드가 3개인 교차 검증을 사용해 만든 모델에 대한 평가를 진행해보자.

from sklearn.model_selection import cross_val_score #분류의 성능을 평가하는 cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")

3개로 나눠 성능을 냈으므로 3개의 성능이 제시되는데, 95이상의 정확도를 기록했다.

3.3.2 오차행렬

분류기의 성능을 평가하는 더 좋은 방법은 오차행렬을 이용하는 것이다.
오차행렬이란 예를들어 클래스 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)

예측에 있어서도 역시나 cv를 사용할 수 있다. 이때 cv는 평가점수를 반환하기 위함이 아닌 각 폴드에서 얻은 예측을 반환하기 위함이다.

예측값도 만들었으니 이제 오차행렬을 만들어야 한다. 오차 행렬을 만들 때는

confusion_matrix 를 이용한다. sklearn.metrics 에서 confusion.matrix 를 임포트 해주면 된다. 인자로는 (타깃클래스, 예측클래스)를 넣는다. 반환하는 값의 행은 실제 클래스, 열은 예측한 클래스를 나타낸다.

from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_5, y_train_pred)

행은 실제, 열은 예측이다. 따라서
예측 음성 예측 양성
실제 음성: 진짜 음성, 거짓 양성
실제 양성: 거짓 음성, 진짜 양성

으로 나타낼 수 있다.

✅ 정밀도

정밀도=TP/(TP+FP)정밀도 = TP / (TP + FP), 양성이라고 예측한 것 중 실제 양성인 비율.

정밀도는 양성 예측의 정확도를 나타내는 것으로 진짜 양성 / (진짜 양성 + 거짓 양성) 을 말한다.

✅ 재현율

$재현율 = TP / (TP + FN) , 실제 양성인 것 중 양성이라고 예측한 비율.

재현율은 분류기가 정확하게 감지한 양성 샘플의 비율이다. 민감도, 진짜 양성 비율과도 같다. 진짜 양성 / (진짜 양성 + 거짓 음성) 으로 계산.

3.3.3 정밀도와 재현율

from sklearn.metrics import precision_score, recall_score

precision_score(y_train_5, y_train_pred)

정밀도의 값이다.

recall_score(y_train_5, y_train_pred)

재현율의 값이다.

✅ F1 점수 : 정밀도와 재현율의 조화평균

역수에 대한 합에 역수를 취한 것이다. F1 점수는 f1_score() 로 추출한다.

from sklearn.metrics import f1_score

f1_score(y_train_5, y_train_pred)

어린아이에게 안전한 동영상을 걸러내는 분류기는 낮은 재현율 - 높은 정밀도를 선호할 것이다.

또다른 예로, 좀도둑을 잡아내는 분류기를 만든다고 해보자. 재현율이 99%라면 정밀도가 30%만 되어도 좋을 수도 있다. (거의 모든 좀도둑 잡기) 그러나 재현율을 올리면 정밀도가 줄고 그 반대도 마찬가지라, 이를 정밀도/재현율 트레이드오프 관계라 한다.

3.3.4 정밀도/재현율 트레이오프

다음과 같은 그림에서 정밀도/재현율 트레이드오프를 따져보자.

임곗값이 가운데 화살표라 했을 때 정밀도는 80 (양성 예측 5중 진짜 양성4) 재현율은 67 (진짜 양성 6개 중 예측양성 4개) 이다.

임곗값을 오른쪽으로 옮길 경우 정밀도는 100으로 증가 (예측 3 중 진짜 양성 3) 재현율은 50 (진짜 양성 6중 예측 3) 이다.

사이킷런에서 decision_function() 메서드를 활용하면 샘플의 점수를 얻을 수 있다.

y_scores = sgd_clf.decision_function([some_digit])
y_scores

threshold = 0
y_some_digit_pred = (y_scores > threshold)

y_some_digit_pred

여기서 임곗값을 높이면 False 를 반환한다.

결과적으로 임곗값을 높이면 재현율은 줄어든다는 것을 알 수 있다.

적절한 임곗값은 cross_val_predict() 를 이용해 훈련 세트에 있는 모든 샘플의 점수를 구한다음 precision_recall_curve() 를 이용하여 모든 임곗값에 대한 정밀도와 재현율을 계산한다.

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_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)


plot_precision_recall_vs_threshold(precisions, recalls, thresholds)

plt.show()

🤔 좋은 정밀도 / 재현율 트레이드오프를 선택하려면 어떻게 해야하나?
➡️ 재현율에 대한 정밀도 곡선을 그리자!

재현율 0.8 정도에서 급격하게 줄어들기 시작하는 정밀도를 감안하여 이 하강점 지점을 트레이드 오프로 선택하는 것이 좋다.

누가 나보고 '정밀도 90을 달성하는 코드를 짜라'고 하면 어떤 코드를 짜야하는지 보자.

# np.argmax() 는 첫번째 True 값을 의미. 
threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)]

# 정밀도 90이 넘는 
y_train_pred_90 = (y_scores >= threshold_90_precision)

# 정밀도 
precision_score(y_train_5, y_train_pred_90)
# 재현율 
recall_score(y_train_5, y_train_pred_90)

3.3.5 ROC 곡선

ROC곡선은 (수신기 조작특성) 거짓양성비율 FPR 에 대한 진짜 양성 비율 TPR (=재현율) 의 곡선이다.

FPR 은 양성으로 잘못 분류된 음성 샘플의 비율이며, 1에서 음성으로 정확하게 분류된 음성 샘플의 비율(TNR) 을 뺸 값이다.

TNR 을 특이도라고 한다. 따라서 ROC 곡선은 재현율(분모)에 대한 1- 특이도이기도 하다.

roc_curve()를 활용하면 fpr, tpr 을 다 계산할 수 있다.

from sklearn.metrics import roc_curve

#thresholds 는 임곗값.
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)

TPR 에 대한 FPR 곡선을 보자. 재현율 TPR 이 높을수록 거짓양성 FPR 또한 늘어난다. 점선은 완전한 랜덤 분류기이고, 따라서 좋은 분류기는 점선에서 최대한 멀리 떨어져야 한다. (거짓양성비율은 적은데 진짜 양성 비율은 높은 상황)

이를 곡선 아래의 면적과 같다고 봐도 무방하다! 이 면적을 AUC 라 한다. roc_auc_score() 을 이용하면 이를 계산할 수 있다.

from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_5, y_scores)

#랜덤포레스트, 랜덤포레스트는 점수를 precit_proba() 로 얻는다. 
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(n_estimators=100, random_state=42)

#데이터에 대한 점수 얻기, X훈련 데이터와 y가 5인 것. 반환하는 것은 어떤 이미지가 5일 확률. (배열로 표현)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3,
                                    method="predict_proba")
                                    
# y_socres_forest 에 5일 확률 저장
y_scores_forest = y_probas_forest[:, 1] # 점수 = 양성 클래스의 확률
# fpr, tpr, 임곗값 반환. 매개변수는 레이블(5인 데이터)과 점수여야 함. 
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5,y_scores_forest)

랜덤포레스트 실선이 더 모서리에 가까워 ROC AUC 점수가 높다.


(매개변수에 라벨과 점수- 확률을 넣어준다.)

3.4 다중 분류

이제까지는 이진분류 (5냐 아니냐)를 사용했지만 드디어 다중 분류기를 사용할 차례다!

물론 이진분류기를 이용해 다중 클래스를 분류할 수 있다!

✅ OvR
특정 숫자 하나만 분류하는 숫자별 이진 분류기 10개를 훈련시키기
✅ OvO
숫자의 조합 (0과1, 0과2..) 마다 이진 분류기 훈련. (n-1)n/2 개의 분류기.

다중 클래스 분류 작업에 이진분류 알고리즘을 선택하면 자동으로 OvO, OvR 중 하나를 실행한다. 서포트벡터머신 분류기를 테스트해보자.

from sklearn.svm import SVC

svm_clf = SVC(gamma="auto", random_state=42)
# 1000개씩 훈련하는데 더이상 5만이 아닌 모든 데이터를 훈련. 자동으로 OvO 를 선택학 된다. 
svm_clf.fit(X_train[:1000], y_train[:1000]) 

내부에서 사이킷런이 OvO 전략을 이용해 45개의 이진분류기를 훈련시키고 각각의 결정 점수를 얻어 가장 높은 클래스를 선택하는 것이다!

some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores

decision_function() 메서드를 호출하면 샘플당 10개의 점수가 반한된다. some_digit 은 현재 5인 클래스인데, 점수가 어떤지 보자.

5가 9.5 로 가장 점수가 높다!

OvO나 OvR 중 특정을 강제하고 싶다면 OneVsOneClassifier 이나 OneVsRestClassifier 를 사용하자. 이진분류기 인스턴스를 만들어 객체 생성 시 전달하면 끝이다!

from sklearn.multiclass import OneVsRestClassifier
#SVC 전달.
ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
ovr_clf.fit(X_train[:1000], y_train[:1000])
ovr_clf.predict([some_digit])

OvR 를 선택하도록 강제했다.

  • SGD 훈련 코드
#역시나 5가 아닌 전체 y 값 
sgd_clf.fit(X_train, y_train)

#CV 점수.
cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")

#스케일 조정하여 점수 올리기. 
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")


결과적으로 가장 중요한 과정은 fit() 으로 훈련 > CV 성능 체크 > 성능 올리는 방법(여러가지) 고민임을 잊지말자! 여기서는 스케일 조정으로 성능 개선을 달성했다.

3.5 에러분석

체크사항

  • 여러 모델 시도
  • 좋은 몇 개를 골라 GridSearchCV 로 하이퍼파라미터튜닝
  • 자동화

를 통해 하나의 모델을 찾았다면 이 모델의 성능을 향상시킬 방법을 찾아보자.

✅ 오차행렬
오차행렬을 만들기 위해선 우선 예측이 필요하고 그 후 confusion_matrix() 를 호출하라고 했다.

# 예측 먼저, X 값으론 정규화 된 데이터를 줌. 
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
# 행렬을 만들고 (정답, 예측값) 
conf_mx = confusion_matrix(y_train, y_train_pred)
conf_mx

plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()

matshow() 함수는 오차행렬을 이미지로 표현하는데 사용한다.

숫자 5가 조금 어두워보인다. 데이터셋에 5이미지 자체가 적거나 다른 숫자만큼 잘 분류하지는 못한다는 뜻! 이럴 때는 비율을 찍어주자.

row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums

np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
save_fig("confusion_matrix_errors_plot", tight_layout=False)
plt.show()

3.6 다중 레이블 분류

예컨대 사진 한장이라는 하나의 샘플에서 여러 개의 클래스를 출력해야 할 때도 있다. 분류기가 앨리스, 밥, 찰리를 인식하도록 훈련되었다면 ('앨리스 있음, 밥 없음, 찰리 있음') 등 이진 꼬리표를 출력하는 분류 시스템을 다중 레이블 분류 시스템이라고 한다.

from sklearn.neighbors import KNeighborsClassifier

# 숫자 7, 8, 9
y_train_large = (y_train >= 7)
# 숫자 1, 3, 5, 7, 9 
y_train_odd = (y_train % 2 == 1)
# 다중 타깃 배열 
y_multilabel = np.c_[y_train_large, y_train_odd]

knn_clf = KNeighborsClassifier()
# 학습 입력변수와 다중 타깃을 넣어주어 학습. (KneighborsClassifier은 다중 레이블 분류를 지원한다.) 
knn_clf.fit(X_train, y_multilabel)

5값인 some_digit 에 대해서 예측해보자.

knn_clf.predict([some_digit])

숫자 5는 7보다 크지 않고(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")

(학습시킨 모델, 학습 입력데이터, 다중 타깃) 을 평가를 실행하자.

3.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, y test 
y_train_mod = X_train
y_test_mod = X_test

잡음이 있는 것을 입력, 없는 것을 타깃으로 준다.

# 입력과 타깃으로 훈련. 
knn_clf.fit(X_train_mod, y_train_mod)

# 예측에는 X_test 를 넣어야 한다. 예측된 값을 보면 노이즈 제거가 출력!
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)

profile
Mathematics, Algorithm, and IDEA for AI research🦖

0개의 댓글