(6-5) 머신러닝 기초 - 선형분류 실습

Yongjoo Lee·2021년 1월 15일
0
post-thumbnail

선형분류 실습

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2F0b12cf95-3d62-4ff4-bfb8-fb340744656e%2F%EA%B7%B8%EB%A6%BC5.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2F0b12cf95-3d62-4ff4-bfb8-fb340744656e%2F%EA%B7%B8%EB%A6%BC5.png)

(실습) MNIST 데이터 분류

Precision/Recall Trade-off

사진출처: https://thecleverprogrammer.com/2020/07/26/precision-and-recall/

모델이 판별할 때 모델의 예측값이

  • 어떤 값보다 크다면 positive
  • 작다면 negative

예측할 때 positive 인지 negative 인지를 판별하는 값을 threshhold 라고 한다.

(threshold 값을 높이면 recall은 낮아지고 precision은 높아진다.)

모델에 따라

  • precision 이 높지만 recall 이 낮은 모델
  • precision 이 낮지만 recall 이 높은 모델

경우에 따라서 어떤 것이 더 좋은가 생각해볼 수 있다.

예를 들어

  • precision < recall (precision 보다는 recall 이 더 중요한 경우)
    • 의료진단의 경우
      • FP - 병이 있다고 판단했는데 실제로 병이 없는 경우
        • 큰 리스크가 있지 않음(검사해보고 없으면 그만이기 때문)
      • FN - 병이 없다고 판단했는데 실제로 병이 있는 경우
        • 리스크가 크다!
    • 스팸 분류할 경우
      • FN - 스팸이 아닌 메일이 스팸으로 분류될 경우
        • 사용자는 중요한 메일을 놓칠 수가 있다!
  • precision > recall (recall 보다는 precision이 더 중요한 경우)
    • 시청 연령 제한 영상 분류의 경우
      • FP - 좋은 영상으로 판단했는데 실제로 좋지 않은 영상인 경우

🔥 경우에 따라 precision 혹은 recall 이 더 높은 모델을 사용해야 한다.

Precision/Recall 조절 방법에 대해서 알아보자.

errors[errors == True]
# 48       True
# 132      True
# 138      True
# 173      True
# 244      True
#          ... 
# 59719    True
# 59726    True
# 59731    True
# 59747    True
# 59766    True
# Name: class, Length: 1567, dtype: bool

y_train_pred[48], y_train_5[48]
# (True, False)

👉 49번째 데이터는 5라고 예측을 했지만 실제로는 5가 아닌 경우의 데이터이다.

우선 49번째 데이터에 대해서 확인

some_digit = X_train.loc[48]

y_scores = log_clf.decision_function([some_digit])
y_scores
# array([0.22419047])

📌 decision_function() : 예측값이 결정 경계(decision boundary)로부터 얼마나 떨어져있는지 반환

some_digit_image = some_digit.values.reshape(28, 28)
plt.imshow(some_digit_image, cmap=mpl.cm.binary)
plt.axis("off")

save_fig("some_digit_plot")
plt.show()

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2F3ab92938-9696-4c85-87c7-fc31c1065229%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2F3ab92938-9696-4c85-87c7-fc31c1065229%2Fimage.png)

threshold = 0
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
# array([ True])

threshold = 0.5
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
# array([False])

👉 threshold 값이 0 인경우에는 True 이지만, 0.5 로 조정했을 경우 False로 바뀜

이제 전체 데이터에 대해서 확인

y_scores = cross_val_predict(log_clf, X_train, y_train_5, cv=3,
                             method="decision_function")

y_scores.shape
# (60000,)
from sklearn.metrics import precision_recall_curve

precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)

precisions.shape
# (59897,)

recalls.shape
# (59897,)

thresholds.shape
# (59896,)
def plot_precision_vs_recall(precisions, recalls):
    plt.plot(recalls, precisions, "b-", linewidth=2)
    plt.xlabel("Recall", fontsize=16)
    plt.ylabel("Precision", fontsize=16)
    plt.axis([0, 1, 0, 1])
    plt.grid(True)

plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
save_fig("precision_vs_recall_plot")
plt.show()

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2Fd68fec62-9ad7-44f2-9efb-e8d4b7836671%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2Fd68fec62-9ad7-44f2-9efb-e8d4b7836671%2Fimage.png)

👉

  • x는 Recall, y는 Precision
  • 각각의 점은 threshold
    • threshold 가 변함에 따라서 precision과 recall 값이 변하게 된다.
    • 특정 지점이 지나면 precision이 급격히 감소된다.

🔥 precision 과 recall이 가져야할 최소값을 만족하는 threshold를 설정한다!

다중 분류 (Multiclass Classification)

from sklearn.linear_model import LogisticRegression
softmax_reg = LogisticRegression(multi_class="multinomial",solver="lbfgs", C=10)
softmax_reg.fit(X_train, y_train)
# LogisticRegression(C=10, multi_class='multinomial')

softmax_reg.predict(X_train)[:10]
# array([5, 0, 4, 1, 9, 2, 1, 3, 1, 4], dtype=uint8)

😄 각각의 클래스가 비슷한 분포를 가지고 있기 때문에 Accuracy를 구하는 것이 나쁘지 않다!

from sklearn.metrics import accuracy_score
y_pred = softmax_reg.predict(X_test)
accuracy_score(y_test, y_pred)
# 0.9243

Data Augmentation

📌 shift image 함수 정의

이미지를 이동시킴

from scipy.ndimage.interpolation import shift

def shift_image(image, dx, dy):
    image = image.reshape((28, 28))
    shifted_image = shift(image, [dy, dx], cval=0, mode="constant")
    return shifted_image.reshape([-1])
image = X_train.loc[1000].values
shifted_image_down = shift_image(image, 0, 5)
shifted_image_left = shift_image(image, -5, 0)

plt.figure(figsize=(12,3))
plt.subplot(131)
plt.title("Original", fontsize=14)
plt.imshow(image.reshape(28, 28), interpolation="nearest", cmap="Greys")
plt.subplot(132)
plt.title("Shifted down", fontsize=14)
plt.imshow(shifted_image_down.reshape(28, 28), interpolation="nearest", cmap="Greys")
plt.subplot(133)
plt.title("Shifted left", fontsize=14)
plt.imshow(shifted_image_left.reshape(28, 28), interpolation="nearest", cmap="Greys")
plt.show()

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2Fb8a568e6-93e3-4834-afb8-e1136617d007%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2Fb8a568e6-93e3-4834-afb8-e1136617d007%2Fimage.png)

학습 데이터의 각 이미지를 변환

X_train_augmented = [image for image in X_train.values]
y_train_augmented = [label for label in y_train.values]

for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
    for image, label in zip(X_train.values, y_train.values):
        X_train_augmented.append(shift_image(image, dx, dy))
        y_train_augmented.append(label)

X_train_augmented = np.array(X_train_augmented)
y_train_augmented = np.array(y_train_augmented)

X_train_augmented.shape
# (300000, 784)

shuffle_idx = np.random.permutation(len(X_train_augmented))
X_train_augmented = X_train_augmented[shuffle_idx]
y_train_augmented = y_train_augmented[shuffle_idx]

X_train_augmented.shape, X_train.shape
# ((300000, 784), (60000, 784))
softmax_reg_augmented = LogisticRegression(multi_class="multinomial",solver="lbfgs", C=10)
softmax_reg_augmented.fit(X_train_augmented, y_train_augmented)

y_pred = softmax_reg_augmented.predict(X_test)
accuracy_score(y_test, y_pred)
# 0.9279

👉 정확도는 0.9243 이었는데 Data Augmentation 이후 0.9279로 조금 올랐다.

(간단한 방식이기 때문에 영향이 적다)

(실습) Titanic 데이터셋

데이터 준비

import numpy as np
import pandas as pd

train_data = pd.read_csv("train.csv")

train_data.head()

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2Fbcb07837-cbff-42ee-bec9-b7877e32521b%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2Fbcb07837-cbff-42ee-bec9-b7877e32521b%2Fimage.png)

  1. survived : 생존 여부
  2. pclass : 탑승 티켓의 등급
  3. sex : 성별
  4. age : 나이
    • Variable notes - 1살보다 어리면 분수로, 추정치라면 xx.5로 표현됨
  5. sibsp : 형제자매나 배우자가 몇명이 타고 있는지
  6. parch : 부모님이나 자식이 몇명이 타고 있는지
  7. ticket : 티켓 번호
  8. fare : 탑승객이 지불한 요금
  9. cabin : 승무원 번호
  10. embarked : 탑승한 항구

👉 Survived 속성을 목표값으로 설정

train_data.info()

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2Ffc0125a2-929a-4d91-a95f-92e72fe67e7d%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2Ffc0125a2-929a-4d91-a95f-92e72fe67e7d%2Fimage.png)

👉

  • Age, Cabin, Embarked 속성들이 missing value를 가지고 있다.
  • Cabin, Name, Ticket 속성들은 무시한다.
    • Cabin은 missing value가 많아서 도움이 되지 않는다고 판단하여 무시
    • Name, Ticket은 의미가 크지 않고 문자열로 되어있기 때문에 따로 처리를 해주어야 함
train_data.describe()

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2F2fd1857e-6840-4016-8d36-ac73de979a0c%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2F2fd1857e-6840-4016-8d36-ac73de979a0c%2Fimage.png)

👉 대략 40% 미만의 생존자가 존재한다.

train_data["Survived"].value_counts()
# 0    549
# 1    342
# Name: Survived, dtype: int64

train_data["Pclass"].value_counts()
# 3    491
# 1    216
# 2    184
# Name: Pclass, dtype: int64

train_data["Sex"].value_counts()
# male      577
# female    314
# Name: Sex, dtype: int64

train_data["Embarked"].value_counts()
# S    644
# C    168
# Q     77
# Name: Embarked, dtype: int64

missing value 처리

📌 DataFrameSelector 함수 정의

몇가지 사용할 속성만 처리하기 위해 Numerical 속성을 처리

from sklearn.base import BaseEstimator, TransformerMixin

class DataFrameSelector(BaseEstimator, TransformerMixin):
    def __init__(self, attribute_names):
        self.attribute_names = attribute_names
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        return X[self.attribute_names]

Numerical 속성을 처리하는 pipeline을 만든다.

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer

num_pipeline = Pipeline([
    ("select_numeric", DataFrameSelector(["Age", "SibSp", "Parch", "Fare"])),
    ("imputer", SimpleImputer(strategy="median")),
])

num_pipeline.fit_transform(train_data)
# array([[22.    ,  1.    ,  0.    ,  7.25  ],
#        [38.    ,  1.    ,  0.    , 71.2833],
#        [26.    ,  0.    ,  0.    ,  7.925 ],
#        ...,
#        [28.    ,  1.    ,  2.    , 23.45  ],
#        [26.    ,  0.    ,  0.    , 30.    ],
#        [32.    ,  0.    ,  0.    ,  7.75  ]])

👉 Numerical 속성들의 missing value는 중앙값으로 채워줌

📌 MostFrequentImputer 함수 정의

가장 많이 나타나는 값으로 missing value를 채워줌

class MostFrequentImputer(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None):
        self.most_frequent_ = pd.Series([X[c].value_counts().index[0] for c in X],
                                        index=X.columns)
        return self
    def transform(self, X, y=None):
        return X.fillna(self.most_frequent_)
from sklearn.preprocessing import OneHotEncoder

cat_pipeline = Pipeline([
    ("select_cat", DataFrameSelector(["Pclass", "Sex", "Embarked"])),
    ("imputer", MostFrequentImputer()),
    ("cat_encoder", OneHotEncoder(sparse=False)),
])

cat_pipeline.fit_transform(train_data)
# array([[0., 0., 1., ..., 0., 0., 1.],
#        [1., 0., 0., ..., 1., 0., 0.],
#        [0., 0., 1., ..., 0., 0., 1.],
#        ...,
#        [0., 0., 1., ..., 0., 0., 1.],
#        [1., 0., 0., ..., 1., 0., 0.],
#        [0., 0., 1., ..., 0., 1., 0.]])

cat_pipeline.fit_transform(train_data)[0]
# array([0., 0., 1., 0., 1., 0., 0., 1.])

👉 Categorical 속성들의 missing value는 가장 많이 나타나는 값으로 채워주고 one-hot-encoding 진행

마지막으로 Categorical, Numerical 속성들을 통합

from sklearn.pipeline import FeatureUnion
preprocess_pipeline = FeatureUnion(transformer_list=[
    ("num_pipeline", num_pipeline),
    ("cat_pipeline", cat_pipeline),
])

X_train = preprocess_pipeline.fit_transform(train_data)
X_train
# array([[22.,  1.,  0., ...,  0.,  0.,  1.],
#        [38.,  1.,  0., ...,  1.,  0.,  0.],
#        [26.,  0.,  0., ...,  0.,  0.,  1.],
#        ...,
#        [28.,  1.,  2., ...,  0.,  0.,  1.],
#        [26.,  0.,  0., ...,  1.,  0.,  0.],
#        [32.,  0.,  0., ...,  0.,  1.,  0.]])

X_train.shape
# (891, 12)

목표값 벡터 확인

y_train = train_data["Survived"]
y_train
# 0      0
# 1      1
# 2      1
# 3      1
# 4      0
#       ..
# 886    0
# 887    1
# 888    0
# 889    1
# 890    0
# Name: Survived, Length: 891, dtype: int64

logistic regression 을 이용하여 학습 진행

log_clf = LogisticRegression(random_state=0).fit(X_train, y_train)

모델 내에서 살펴보기

a = np.c_[log_clf.decision_function(X_train), y_train, X_train]

df = pd.DataFrame(data=a, columns=["Score", "Survived", "Age", "SibSp", "Parch", "Fare", "Pclass_1", "Pclass_2", "Pclass_3", "Female", "Male", "Embarked_C", "Embarked_Q", "Embarked_S"])

df

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2Fda541e2b-6f2c-42ad-8421-a84ebc1e8e72%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2Fda541e2b-6f2c-42ad-8421-a84ebc1e8e72%2Fimage.png)

점수가 높은 값부터 정렬

df.sort_values(by=['Score'], ascending=False)[:20]

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2Febe5ef3f-4ee3-4d8e-a2ed-302d83415fdc%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2Febe5ef3f-4ee3-4d8e-a2ed-302d83415fdc%2Fimage.png)

👉 생존한 사람들의 대부분이 Score가 높다.

  • pclass_1 의 사람들이 점수가 높다.
  • 여성이 점수가 높다.

점수가 낮은 값부터 정렬

df.sort_values(by=['Score'])[:20]

![https://velog.velcdn.com/images%2Fleeyongjoo%2Fpost%2F304bda4f-423b-4f95-bc3a-1f15969b5e4e%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fleeyongjoo%2Fpost%2F304bda4f-423b-4f95-bc3a-1f15969b5e4e%2Fimage.png)

👉 생존하지 못한 사람들의 대부분이 Score가 낮다.

  • 같이 승선한 형제자매 수가 많은 사람들이 점수가 낮다.
  • pclass_3 의 사람들이 점수가 낮다.
  • 남성이 점수가 낮다.
profile
하나씩 정리하는 개발공부로그입니다.

0개의 댓글