목차
👀, 🤷♀️ , 📜
이 아이콘들을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
INTRO
고등학생과 미국 인구조사국 직원들이 손으로 쓴 70,000 개의 작은 숫자 이미지를 모은 MNIST 데이터셋을 사용할 것이다.
각 이미지에는 어떤 숫자를 나타내는지 레이블링이 되어 있다.
이 데이터셋은 학습용으로 아주 많이 시용되기 때문에 머신러닝 분야의 ‘Hello World’ 라고 불린다.
이를 아래와 같은 방법들로 분류할 것이다!
📜 필요한 모듈 불러오기
# 파이썬 ≥3.5 필수
import sys
assert sys.version_info >= (3, 5)
# 사이킷런 ≥0.20 필수
import sklearn
assert sklearn.__version__ >= "0.20"
# 공통 모듈 임포트
import numpy as np
import os
# 노트북 실행 결과를 동일하게 유지하기 위해
np.random.seed(42)
# 깔끔한 그래프 출력을 위해
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
# 그림을 저장할 위치
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "classification"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
os.makedirs(IMAGES_PATH, exist_ok=True)
def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
print("그림 저장:", fig_id)
if tight_layout:
plt.tight_layout()
plt.savefig(path, format=fig_extension, dpi=resolution)
📜 데이터 셋 불러오기
그럼 먼저 데이터 셋을 불러와보자.
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.keys()
이 데이터 셋을 살펴보기 위해 mnist.keys()
를 사용하면,
- DESCR: 데이터셋을 설명
- data: 샘플이 하나의 행, 특성이 하나의 열로 구성된 배열
- target: 레이블 배열(정답)
- 레이블은 문자열임 -> 대부분 머신러닝 알고리즘은 숫자를 기대하므로를 정수(
uint8
)로 변환 필요.
- 레이블은 문자열임 -> 대부분 머신러닝 알고리즘은 숫자를 기대하므로를 정수(
dict_keys(['data', 'target', 'frame', 'categories', 'feature_names', 'target_names', 'DESCR', 'details', 'url'])
# target 정수 변환
y = mnist["target"]
y = y.astype(np.uint8)
📜 위 배열들 자세히 보기
이미지가 70,000 개 존재
- 각 이미지에는 784 개의 특성 존재
- 이미지가 28 X 28 픽셀이기 때문.
- 개개의 특성은 단순히 흰색 부터 255( 검은색)까지의 픽셀 강도를 나타냄
X, y = mnist["data"], mnist["target"]
X.shape
#(70000, 784)
y.shape
#(70000,)
📜 targe 자세히 보기
y = mnist["target"]
이때, y[0]은 아래와 같다
그러므로 이 이미지의 정답 레이블은 5여야 한다.
y[0]
# '5'
[훈련세트, 테스트 세트로 나누기]
더 알아보기
- 훈련 세트: 이미 섞여 있어서 모든 교차 검증 폴드를 비슷하게 만듦.
- 하나의 폴드라도 특정 숫자가 누락되면 안 됨
이 MNIST 데이터셋은 이미 나뉘어 있음.
- 훈련 세트(앞쪽 60,000 개 이미지)
- 테스트 세트 (뒤쪽 10,000 개 이미지)
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
이진 분류기 훈련
1. 타깃 벡터 만들기
이진분류기 binary classfier: 두개의 답 중 하나를 식별하는 것
즉 이 문제에 적용시켜보면, 5라는 손글씨를 판별할 때,
- ‘5’
- ‘5 아님’(5가 아닌 다른 정답들)
위와 같은 두 개의 클래스로 구분됨.
그럼 이제 이를 위해 타깃 벡터를 만들어보겠다
y_train_5 = (y_train == 5) # 5는 True고, 다른 숫자는 모두 False
y_test_5 = (y_test == 5)
2. 모델 선택
이제 분류 모델을 하나 선택해서 훈련시켜보겠다.
[선택 모델]
확률적 경사 하강법 Stochastic Gradient Descent (SGD)
sklearn
의SGDClassifier
클래스- 매우 큰 데이터셋을 효율적으로 처리하는 장점을 지님
- GD가 한 번에 하나씩 훈련 샘플을 독립적으로 처리하기 때문(위 링크에 나와있음)
📜 데이터셋을 섞는 이유 (독립적 처리의 이유)
어떤 학습 알고리즘은 훈련 샘플의 순서에 민감해서 많은 비슷한 샘플이 연이어 나타나면 성능이 나빠짐 데이터셋을 섞으면 이런 문제를 방지가능
➡️ SGD가 샘플을 섞어야 하는 대표적인 경우임.
사이킷런의 SGDClassifier와 SGDRegressor는 기본적으로 에포크(전체 트레이닝 데이터세트에 대해 트레이닝해야 하는 횟수)마다 관련 데이터를 다시 섞음
하지만 반대로 섞으면 오히려 나빠지는 경우가 있다
(섞는게 좋지 않은 예)
시계열 데이터
- 주식가격
- 날씨 예보
➡️ 이 경우는 다음 포스트에서 살펴보겠다
그럼 이제 SGDClassifier 모델을 만들고 전체 훈련 세트를 사용해 훈련시켜보겠다.
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)
sgd_clf.fit(X_train, y_train_5)
SGDClassifier(random_state=42)
성능 측정
오차행렬
= confusion matrix
기본적인 아이디어는 클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 것임.
예를들어 분류기가 숫자 5의 이미지를 3으로 잘못 분류한 횟수를 알고 싶다면 오차 행렬의 5행 3열을 보면 됨.
예측값 만들기
오차 행렬을 만들려면 실제 타깃과 비교할 수 있도록 먼저 예측값을 만들어야 함.
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)
오차행렬 만들기
- TN : 예측값을 Negative 값 0으로 예측했는데 실제 값 역시 Negative 값 0 일때
- FP : 예측값을 Positive 값 1으로 예측했는데 실제 값은 Negative 값 0 일때
- FN : 예측값을 Negative 값 0으로 예측했는데 실제 값은 Positive 값 1 일때
- TP : 예측값을 Positive 값 1으로 예측했는데 실제 값 역시 Positive 값 1 일때
confusion_matrix()
함수 사용
- 타깃 클래스
y_train_S
- 예측 클래스
y_train_pred
를 넣고 호출
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix
confusion_matrix(y_train_5, y_train_pred)
array([[53892, 687],
[ 1891, 3530]])
분류기 지표
정밀도 precision
오차 행렬이 많은 정보를 제공해주지만 가끔 더 요약된 지표가 필요할 때도 있음.
이를 위해 양성 예측의 정확도를 살표본 것을 분류기의 정밀도라고 함.
(정밀도 식)
\(TP / (TP+FP)\).
(구현)
from sklearn.model_selection import cross_val_predict
precision_score(y_train_5, y_train_pred) # == 4096 / (4096 + 1522)
0.8370879772350012
# 정밀도가 꽤 높다
재현율 recall
확실한 양성 샘플 하나만 하면 간단히 완벽한 정밀도를 얻을 수 있지만,
이는 분류기가 다른 모든 양성 샘플을 무시하기 때문에 그리 유용하지 않음.
그러므로 정밀도는 재현율 이라는 또 른 지표와 같이 사용하는 것이 일반적입니다.
재현율: 분류기가 정확하게 감지한 양성 샘플의 비율
= 민감도
= 진짜 양성 비율
(재현율 식)
\(TP / (TP+FN)\).
(구현)
from sklearn.model_selection import cross_val_predict
recall_score(y_train_5, y_train_pred) # == 4096 / (4096 + 1325)
0.6511713705958311
# 반면 재현률은 낮은 수치를 보인다
# 정밀도로는 예측을 단정하기 어렵다는 것을 보여준다
\(F_1\) 점수
정밀도와 재현율을 하나의 숫자로 만든 것.
이는 두 분류기의 성능비교에 유용함.
\(F_1\) 점수는 정밀도와 재현율의 조화 평균임
- 정밀도와 재현율이 비슷한 분류기에서는 \(F_1\) 점수가 높음
(구현)
from sklearn.metrics import fl_score
fl_score (y_train_S, y_train_pred)
0.7420962043663375
📜 어떤 특정 점수가 항상 제일 중요한가?
아니다! 상황에 따라 다르다.
상황에 따라 정밀도가 중요할 수도 있고 재현율이 중요할 수도 있다.
[정밀도가 더 중요한 경우]
어린이이에게 안전한 동영상을 걸러 는 분류기를 훈련시키는 상황
➡️ 재현율은 높으나 정말 나쁜동영상이 몇 개 노출되는것보다
- 좋은동영상이 많이 제외되더라도 (낮은재현률)
- 안전한 것들만 노출시키는 (높은 정밀도) 분류기를 선호
정밀도/재현율 트레이드오프
위와같이 데이터마다 중요해지는 지표가 다르고 두 지표를 모두 높이는 것은 불가능하다.
즉 정밀도/ 재현율 트레이드 오프는 정밀도를 올리면 재현율이 줄고 그 반대도 마찬가지라는 것을 뜻한다.
SGDClassifier가 분류를 어떻게 결정하는지 살펴보며 이 트레이드오프를 이해해보자.
이 분류기는 결정 함수 decision function 를 tk용하여 각 샘플의 점수를 계산한다.
- 이 점수가 임겟값보다 크면: 샘플을 양성 클래스에 할당
- 이 점수가 임겟값보다 작으면: 샘플을 음성 클래스에 할당
이 점수에 따라, 가장 낮은 점수부터 가장 높은 점수까지 몇 개의 숫지를 나열해보면,
[출처] 핸즈온 머신러닝
양성 예측: 임겟값 오른쪽
- 4 개의 진짜양성(실제 숫자 5)
- 하나의 거짓 양성(실제 숫자 6)
➡️ 이 임겟값에서 정밀도는 80%(5 개 중 4개)
전체를 통한 예측: 실제 숫자 5는 6개고 분류기는 4개
➡️ 재현율은 67%( 6개 중 4개)입니다.
임계값 높여 예측: 이번에 임곗값을 높이면 임값을 오른쪽 화살표로 옮기면
- 정밀도) 거짓 양성 숫자 이 진짜 음성이 되어 정밀도가 높아짐 -> 100이 됨
- 재현율) 진짜 양성 하나가 거짓 음성이 되었으므로 재현율이 50 로 줄어듦
➡️ 반대로 임겟값을 내리면 재현율이 높아지고 정밀도가 줄어듭니다.
사이킷런에서 임겟값을 직접 지정할 수는 없지만,
예측에 사용한 점수는 확인 가능.
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])
SGDClassifier 의 임겟값이 0 이므로 위 코드는 True 를 반환
👀 재현율 줄이기(임계값 높이기)
threshold = 8000
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
# array([False])
이 결과는 임겟값을 높이면 재현율이 줄어든다는 것을 보여줌.
이미지가 실제로 숫자 5이고 임계값이 5일 때는 분류기가 이를 감지했지만,
임계값을 8,000으로 높이면 이를 놓치게 됨.
(임곗값을 오른쪽 화살표로 옮긴다고 생각)
[적절한 임곗값 정하는 법]
cross_val_predict()
함수 사용
- 훈련 세트에 있는 모든 샘플의 점수를 구해야 함.
- 하지만 이번에는 예측 결과가 아닌 결정 점수를 반환 받도록 지정해야 함.
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")
이 점수로 precision_recall_curve()
함수를 사용하여 가능한 모든 임곗값에 대해 정밀도와 재현율 계산 가능.
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
📜 정밀도 재현률 시각화
이제 matplotlib을 통해 임곗값의 함수로 정밀도와 재현률 시각화
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)
plt.legend(loc="center right", fontsize=16) # Not shown in the book
plt.xlabel("Threshold", fontsize=16) # Not shown
plt.grid(True) # Not shown
plt.axis([-50000, 50000, 0, 1]) # Not shown
recall_90_precision = recalls[np.argmax(precisions >= 0.90)]
threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)]
plt.figure(figsize=(8, 4)) # Not shown
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.plot([threshold_90_precision, threshold_90_precision], [0., 0.9], "r:") # Not shown
plt.plot([-50000, threshold_90_precision], [0.9, 0.9], "r:") # Not shown
plt.plot([-50000, threshold_90_precision], [recall_90_precision, recall_90_precision], "r:")# Not shown
plt.plot([threshold_90_precision], [0.9], "ro") # Not shown
plt.plot([threshold_90_precision], [recall_90_precision], "ro") # Not shown
save_fig("precision_recall_vs_threshold_plot") # Not shown
plt.show()
다중 분류
= multiclass classifier
= multinomial classifier
- 이진 분류: 두 개의 클래스를 구별
- 다중 분류(다항 분류): 둘 이상의 클래스를 구별 가능
OvR(OvA)
[과정]
1) 특정 숫자 하나만 구분하는 숫자별 이진분류기 10 (0 에서 부터 까지)를 훈련시킴
2) 이를 바탕으로 클래스가 10 개인 숫자 이미지 분류 시스템 만듦
3) 이미지를 분류할 때 각 분류기의 결정 점수 중에서 가장 높은 것을 클래스로 선택
대부분 이를 사용
OvO
[과정]
은 과 1 구별 2 구별 2 구별 등과같이 각숫자의 조합마다 이진 분류기를 훈련시킴
[분류기 개수]
클래스가 N개라면 분류기는 N x(N-1)/2 개 필요
-> MNIST 문제에서는 45 개의 분류기 훈련 필요
[장점]
각 분류기 의 훈련에 전체 훈련 세트 중 구별할 두 클래스에 해당히는 샘플만 필요
- (서포트 벡터 머신 같은) 일부 알고리 은 훈련 세트의 크기에 민감해서 큰 훈련 에서 몇 개의 분류기를 훈련시키는 것보다 작은 훈련 세트에서 많은 분류기를 훈련시키는 쪽이 빠르므 이를 선호
👀코드 보기
from sklearn.svm import SVC
svm_clf = SVC(gamma="auto", random_state=42)
svm_clf.fit(X_train[:1000], y_train[:1000]) # y_train_5이 아니라 y_train입니다 # 1)
svm_clf.predict([some_digit]) # 2)
array([5], dtype=uint8)
이 코드는,
1) 5를 구별한 타깃 클래스 y_train_5 대신 0에서 9까지의 원래 타깃 클래스 y_train 를 사용해 SVC를 훈련시킴.
-> 즉 이진화를 안함
2) 그런 다음 예측 하나를 만듦 (결과를 보니 정확히 맞췄다는 것을 볼 수 있음)
[내부]
사이킷런이 OvO 전략을 사용해 1) 10 개의 이진분류기를훈련시키고
2) 각각의 결정 점수를 얻어 점수가 가장 높은 클래스를 선택함.
아래 코드로 내부를 확인해보면
some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores
array([[ 2.81585438, 7.09167958, 3.82972099, 0.79365551, 5.8885703 ,
9.29718395, 1.79862509, 8.10392157, -0.228207 , 4.83753243]])
설명과 같다는 것을 알 수 있다
from sklearn.svm import SVC
svm_clf = SVC(gamma="auto", random_state=42)
svm_clf.fit(X_train[:1000], y_train[:1000]) # y_train_5이 아니라 y_train입니다
svm_clf.predict([some_digit])
가장 높은 점수가 클래스 5에 해당하는 것이다.
array([5], dtype=uint8)
성능 향상
데이터의 종류로 알아보는 실제 머신러닝 과정은 이렇다.
우린 가능성이 높은 모델을 하나 찾았다고 가정하고(1단계 완료) 이 모델의 성능을 향상시킬 방법을 찾아보자.
에러분석
성능을 향상시키는 방법 중 하나가 에러분석이다.
에러분석을 위해 먼저 오차행렬(위에서 배웠다!)부터 살펴보자
위에서 배운 오차행렬을 보기 편하게 시각화를 하면 아래와 같다.
이 오차 행렬은 대부분의 이미지가 올바르게 분류되었음을 나타내는 대각에 있으므로 매우 좋아보임
그런데,
숫자 5의 이미지가 다른 숫자보다 조금 어두워 보임
➡️ 이는 데이터의 이미지가 적거나,
➡️ 분류기가 다른 숫자 만큼 잘 분류하지 못한다는 것을 의미
👀 오차 행렬 시각화 코드 보기
원래 오차행렬은 이거다
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
array([[5577, 0, 22, 5, 8, 43, 36, 6, 225, 1],
[ 0, 6400, 37, 24, 4, 44, 4, 7, 212, 10],
[ 27, 27, 5220, 92, 73, 27, 67, 36, 378, 11],
[ 22, 17, 117, 5227, 2, 203, 27, 40, 403, 73],
[ 12, 14, 41, 9, 5182, 12, 34, 27, 347, 164],
[ 27, 15, 30, 168, 53, 4444, 75, 14, 535, 60],
[ 30, 15, 42, 3, 44, 97, 5552, 3, 131, 1],
[ 21, 10, 51, 30, 49, 12, 3, 5684, 195, 210],
[ 17, 63, 48, 86, 3, 126, 25, 10, 5429, 44],
[ 25, 18, 30, 64, 118, 36, 1, 179, 371, 5107]])
근데 보기 너무 힘드므로 시각화를 해보자
def plot_confusion_matrix(matrix):
"""If you prefer color and a colorbar"""
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot(111)
cax = ax.matshow(matrix)
fig.colorbar(cax)
plt.matshow(conf_mx, cmap=plt.cm.gray)
save_fig("confusion_matrix_plot", tight_layout=False)
plt.show()
배열에서 가장 큰 은 흰색으로. 장 작은 은 검은색 로 정규화되어 그려짐
그럼 이제 에러에 초점을 맞춰보자
➡️ 오차 행렬의 각 값을 대응되는 이미지의 개수로 나누어 에러 비율을 비교.
row_sums = conf_mx .sum(axis=l , keepdims=True)
norm_conf_mx = conf_mx / row_sums
이를 시각화 위해 다른 항목은 그대로 유지하고 주대각선만 으로 채워서 그래프를 그려보자
np .fill_diagonal(norm_conf_mx, 0)
plt.matshow(nonn_conf_mx, cmap=plt. cm .gray)
plt. show()
이를 해석해보면
- 행: 실제 클래스
- 열: 예측 클래스
이므로 클래스 의 이 상당히 밝으므로 많은 이미지가로 8로 잘못 분류되었음을 암시
즉 8로 잘못 분류되는 것을 이도록 개선할 필요 있음 ➡️ 8 처럼 보이는 (하지만 실제 8은 아닌) 숫자의 훈련 데이터를 더 많이 모아서 실제 8과 구분하도록 분류기를 학습하게 하는 훈련 필요
다중 레이블 분류
지금까지는 각샘플이 하나의 클래스에만 할당되었음
하지만 분류기가 샘플마다 여러 개 의 클래스를 출력해야 할 때도 있음.
얼굴 인식 분류기를 한번 생각해봅시다. 같은 사진에 여러 사람이 등장한다면 어떻게 해야할까?
인식된 사람마다하나씩 꼬리표 붙여야 한다.
분류기가 앨리스, 밥, 찰리 세 얼굴을 인식하도록 훈련되었다고 가정해보자.
분류기가 앨 리스와 찰리가 있는 사진을 본다면 [1, 0, 1]을 출력해야 할 것이다(즉, ‘앨리스 있음, 밥 없 음, 찰리 있음’).
이처럼 여러 개의 이진 꼬리표를 출력하는 분류 시스템을 다중 레이블 분류 multilabel classification 라고 한다.
이런 모델의 예를 MNISIT에 응용해서 적용해보자.
- (기준 1) 는 숫자가 큰 값 7, 8, 인지
- (기준 2) 홀수 인지
이를 위해 각 숫자 이미지에 두 개의 타깃 레이블이 담긴 y_multilabel 배열을 만듬.
그 다음, KNeighborsClassifier 인스턴스를 만들고 다중 타깃 배열을 사용하여 훈련시킴.
👀코드 보기
from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
KNeighborsClassifier()
이제 5를 통해 예측을 해보자!
그럼 레이블이 두개 출력된다
knn_clf.predict([some_digit])
array([[False, True]])
올바르게 분류되었다! 숫자 5는 크지 않고 (기준 1: False) 홀수 (기준 2:True)이다.
다중 출력 분류
다중 출력 다중 클래스 분류 multioutput-multiclass classification or 다중 출력 분류 multioutput classification
다중 레이블 분류(위에서 했다!)에서 한 레이블이 다중클래스가 될 수 있도록 일반화한 것 (즉, 값을 두 개 이상 가질 수 있음).
이를 위해 이미지에서 잡음을 제거하는 시스템을 만들어보겠다.
- 잡음이 많은 숫자 이미지를 입력으로 받고,
- 깨끗한숫자 이미지를 MNIST 이미지처럼 픽셀의 강도를 담은 배열로 출력.
- 분류기의 출력이 다중 레이블(픽셀당 레이블 1개) 임
- 각 레이블은 값을 여러 개 가짐 (0 부터 255 까지 픽셀 강도).
기존 입력 데이터셋에 노이즈를 추가해서 새로운 입력 데이터를 만들고, 기존 입력 데이터로 라벨을 만든다
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.fit(X_train_mod , y_train_mod)
clean_digit = knn_clf.predict([X_test_mod [some_index]])
plot_digit(clean_digit)