지도학습이란?
지도학습은 명확한 정답이 주어진 데이터를 먼저 학습한 뒤 미지의 정답을 예측하는 방식

이때 학습을 위해 주어진 데이터 세트를 학습 데이터 세트, 머신러닝 모델의 예측 성능을 평가하기 위해 별도로 주어진 데이터 세트를 테스트 데이터 세트로 지칭함

하이퍼 파라미터란?
하이퍼 파라미터는 머신러닝 알고리즘별로 최적의 학습을 위해 직접 입력하는 파라미터들을 통칭하며, 하이퍼 파라미터를 통해 머신러닝 알고리즘의 성능을 튜닝할 수 있음


붓꽃 품종 예측하기
추후에 배우게 될 의사결정트리(Decision Tree)를 활용해 붓꽃의 품종을 분류하는 모델을 만들어 머신러닝의 흐름을 파악해보자!

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 붓꽃 데이터 세트 로딩
iris = load_iris()

# 피처와 레이블
iris_data = iris.data
iris_label = iris.target

학습 데이터로 학습된 모델이 얼마나 뛰어난 성능을 가지는지 평가하기 위해 학습/테스트 데이터 분리함

# 학습/테스트 데이터 분리
x_train, x_test, y_train, y_test = train_test_split(iris_data, iris_label,
                                                   test_size=0.2, random_state=11)
# DecisionTreeClassifier 객체 생성
dt_clf = DecisionTreeClassifier(random_state=11)

# 학습
dt_clf.fit(x_train, y_train)

# 예측
pred = dt_clf.predict(x_test)

# 평가
print('예측 정확도:{0:.4f}'.format(accuracy_score(y_test, pred)))
예측 정확도:0.9333

사이킷런 프레임워크 익히기(지도학습:분류, 회귀)
●사이킷런은 ML 모델 학습을 위해 fit(), 학습된 모델의 예측을 위해 predict() 메서드 제공
●분류 알고리즘은 Classifier, 회귀 알고리즘은 Regressor로 지칭 → 두 개를 합쳐서 Estimator라고 부름
●cross_val_score와 같은 평가함수, GridSearchCV와 같은 하이퍼 파라미터 튜닝을 지원하는 클래스는 Estimator를 인자로 받음

사이킷런의 주요 모듈


Model Selection 모듈 소개
1. 학습/테스트 데이터 세트 분리 - train_test_split()
sklearn.model_selection의 train_test_split()함수

●첫 번째 파라미터로 피처 데이터 세트 입력
●두 번째 파라미터로 레이블 데이터 세트 입력
●test_size: 전체 데이터에서 테스트 데이터 세트 크기를 얼마로 샘플링할 것인가를 결정함. 디폴트는 0.25, 즉 25%임
●train_size: 전체 데이터에서 학습용 데이터 세트 크기를 얼마로 샘플링할 것인가를 결정함. test_size parameter를 통상적으로 사용하기 때문에 train_size는 잘 사용되지 않음
●shuffle: 데이터를 분리하기 전에 데이터를 미리 섞을지를 결정함, 디폴트는 True임
●random_state: 호출할 때마다 동일한 학습/테스트용 데이터 세트를 생성하기 위해 주어지는 난수값임, train_test_split()은 호출 시 무작위로 데이터를 분리하므로 random_state를 지정하지 않으면 수행할 때마다 다른 학습/테스트 용 데이터를 생성함

하지만 이 방법 역시 과적합(Overfitting)이라는 약점이 존재함
여기서 과적합이란 모델이 학습 데이터에만 과도하게 최적화되어, 실제 예측을 다른 데이터로 수행할 경우에는 예측 성능이 과도하게 떨어지는 경우를 말함, 즉 고정된 학습 데이터로 학습하고 고정된 테스트 데이터를 통해 모델의 성능을 확인하고 수정하는 과정을 반복하면, 결국 내가 만든 모델은 테스트 데이터에만 잘 동작하는 모델이 된다. 이 경우에는 테스트 데이터에 과적합되어 다른 실제 데이터를 가지고 예측을 수행하면 엉망인 결과가 나와버림
→ 이를 해결하기 위해 교차검증을 사용

2. 교차검증
간략하게 설명하자면 본고사를 치르기 전에 모의고사를 여러 번 보는 것임. 즉, 본고사가 테스트 데이터 세트에 대해 평가하는 거라면 모의고사는 교차검증에서 많은 학습과 검증 세트에서 알고리즘 학습과 평가를 수행하는 것

(원칙적으로 train_test_split으로 학습과 테스트 데이터 세트로 분리 후 학습 데이터를 학습과 검증 데이터 세트로 나눠 1차 평가)

→교차검증은 별도의 여러 세트로 구성된 학습 데이터 세트와 검증 데이터 세트에서 학습과 평가를 수행하는 것
⇛ML모델의 성능 평가는 교차 검증 기반으로 1차 평가를 한 뒤 최종적으로 테스트 데이터 세트에 적용해 평가하는 프로세스이다.

 

(iris데이터는 데이터 세트가 적기 때문에 교차검증이 어떻게 적용되는지를 살펴보기 위해서 학습 데이터에서 검증 데이터 세트를 안나누고 전체 데이터 세트로 교차검증을 수행했다. GridSearchCV는 원칙대로 적용)

1) K 폴드 교차검증
K 폴드 교차검증은 가장 보편적으로 사용되는 교차검증 기법으로, K개의 데이터 폴드 세트를 만들어서 K번만큼 각 폴드 세트에 학습과 검증 평가를 반복적으로 수행하는 방법
→K개의 예측 평가를 구했으면 이를 평균해서 K 폴드 평가 결과로 반영

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import KFold
import numpy as np

iris = load_iris()
features = iris.data
label = iris.target

dt_clf = DecisionTreeClassifier(random_state=156)

# 5개의 폴드 세트로 분리하는 KFold 객체와 폴드 세트별 정확도를 담을 리스트 객체 생성
kfold = KFold(n_splits=5)
cv_accuracy = []

n_iter = 0 # for loop시 몇 번 반복했는지 확인하기 위해 n_iter 객체 생성
# KFold 객체의 split()를 호출하면 학습용, 검증용 테스트 데이터로 분할할 수 있는 인덱스를 array로 반환
for train_index, test_index in kfold.split(features):
    # kfold.split()으로 반환된 인덱스를 이용해 학습용, 검증용 테스트 데이터 추출
    x_train, x_test = features[train_index], features[test_index]
    y_train, y_test = label[train_index], label[test_index]
    # 학습 및 예측
    dt_clf.fit(x_train, y_train)
    pred = dt_clf.predict(x_test)
    # 평가(정확도)
    accuracy = np.round(accuracy_score(y_test, pred), 4)
    # 폴드 세트별 정확도를 담을 리스트 객체에 정확도 추가
    cv_accuracy.append(accuracy)
    
    n_iter += 1
    train_size = x_train.shape[0]
    test_size = x_test.shape[0]
    print('\n#{0} 교차검증 정확도:{1}, 학습 데이터 크기:{2}, 검증 데이터 크기:{3}'.format(n_iter, accuracy, train_size, test_size))
    # split()이 어떤 값을 실제로 반환하는지 확인해 보기 위해 검증 데이터 세트의 인덱스도 추출해봄
    print('#{0} 검증 세트 인덱스:{1}'.format(n_iter, test_index))

# 개별 iteration별 정확도를 합하여 평균 정확도 계산
print('\n## 평균 검증 정확도:', np.mean(cv_accuracy))


2) Stratified K 폴드
Stratified K 폴드는 불균형한 분포도를 가진 레이블 데이터 집합을 위한 K 폴드 방식
→Stratified K 폴드는 원본 데이터의 레이블 분포를 먼저 고려한 뒤 이 분포와 동일하게 학습과 검증용 테스트 데이터를 분배

StratifiedKFold를 사용하는 방법은 KFold를 사용하는 방법과 비슷하다. 단 하나 큰 차이는 StratifiedKFold는 레이블 데이터 분포도에 따라 학습/검증용 테스트 데이터를 나누기 때문에 split() 메서드에 인자로 피처 데이터 세트뿐만 아니라 레이블 데이터 세트도 반드시 필요함

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import StratifiedKFold
import numpy as np

iris = load_iris()
features = iris.data
label = iris.target

dt_clf = DecisionTreeClassifier(random_state=156)

# 3개의 폴드 세트로 분리하는 StratifiedKFold 객체와 폴드 세트별 정확도를 담을 리스트 객체 생성
skfold = StratifiedKFold(n_splits=3)
cv_accuracy = []

n_iter = 0 # for loop시 몇 번 반복했는지 확인하기 위해 n_iter 객체 생성
# StratifiedKFold 객체의 split()를 호출하면 학습용, 검증용 테스트 데이터로 분할할 수 있는 인덱스를 array로 반환
# StratifiedKFold의 split() 호출시 반드시 레이블 데이터 세트도 추가 입력 필요
for train_index, test_index in skfold.split(features, label):
    # skfold.split()으로 반환된 인덱스를 이용해 학습용, 검증용 테스트 데이터 추출
    x_train, x_test = features[train_index], features[test_index]
    y_train, y_test = label[train_index], label[test_index]
    # 학습 및 예측
    dt_clf.fit(x_train, y_train)
    pred = dt_clf.predict(x_test)
    # 평가(정확도)
    accuracy = np.round(accuracy_score(y_test, pred), 4)
    # 폴드 세트별 정확도를 담을 리스트 객체에 정확도 추가
    cv_accuracy.append(accuracy)
    
    n_iter += 1
    train_size = x_train.shape[0]
    test_size = x_test.shape[0]
    print('\n#{0} 교차검증 정확도:{1}, 학습 데이터 크기:{2}, 검증 데이터 크기:{3}'.format(n_iter, accuracy, train_size, test_size))
    # split()이 어떤 값을 실제로 반환하는지 확인해 보기 위해 검증 데이터 세트의 인덱스도 추출해봄
    print('#{0} 검증 세트 인덱스:{1}'.format(n_iter, test_index))

# 개별 iteration별 정확도를 합하여 평균 정확도 계산
print('\n## 평균 검증 정확도:', np.round(np.mean(cv_accuracy), 4))

그럼 KFold와 StratifiedKFold는 언제 써야 할까?

  • KFold
    • 회귀 문제(회귀의 결정값은 이산값 형태의 레이블이 아니라 연속된 숫자값이기 때문에 결정값 별로 분포를 정하는 의미가 없기 때문에 Stratified K 폴드가 지원되지 않음)에서의 교차 검증
  • StratifiedKFold
    • 레이블 데이터가 왜곡됐을 경우 반드시
    • 분류 문제에서의 교차 검증


사이킷런은 교차검증을 좀 더 편리하게 수행할 수 있게 해주는 API를 제공함 → cross_val_score()

3) cross_val_score()

cross_val_score(estimator, X, y=None, scoring=None, cv=None, n_jobs=1, verbose=0, fit_params=None, pre_dispatch='2*n_jobs') → 이 중 estimator, X, y, scoring, cv가 주요 파라미터임
●estimator: 사이킷런의 분류 알고리즘 클래스인 Classifier 또는 회귀 알고리즘 클래스인 Regressor를 의미
●X: 피처 데이터 세트
●y: 레이블 데이터 세트
●scoring: 예측 성능 평가 지표
●cv: 교차검증 폴드 수

cross_val_score()는 분류 문제인 경우 Stratified K 폴드 방식으로 작동하고, 회귀 문제인 경우 K 폴드 방식으로 작동

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
import numpy as np

iris = load_iris()
features = iris.data
label = iris.target

dt_clf = DecisionTreeClassifier(random_state=156)

# 성능 지표는 정확도(accuracy), 교차검증 세트는 3개
scores = cross_val_score(dt_clf, features, label, scoring='accuracy', cv=3)
print('교차검증별 정확도:', np.round(scores, 4))
print('평균 검증 정확도:', np.round(np.mean(scores), 4))


4) 교차검증과 최적의 하이퍼파라미터 튜닝을 한번에 - GridSearchCV
사이킷런은 GridSearchCV를 이용해 분류와 회귀 알고리즘에 사용되는 하이퍼 파라미터를 순차적으로 입력하면서 편리하게 최적의 하이퍼파라미터를 도출할 수 있는 방안을 제공(최적의 하이퍼파라미터를 교차검증을 통해 추출)

<GridSearchCV의 주요 파라미터>
●estimator: 사이킷런의 분류 알고리즘 클래스인 Classifier 또는 회귀 알고리즘 클래스인 Regressor를 의미
●param_grid: key + 리스트 값을 가지는 딕셔너리가 주어짐. estimator의 튜닝을 위해 하이퍼파라미터명과 사용될 여러 하이퍼파라미터 값을 지정
●scoring: 예측 성능 평가 지표
●cv: 교차검증 폴드 수
●refit: 디폴트가 True이며 True로 생성시 최적의 하이퍼파라미터를 찾은 뒤, 입력된 estimator 객체를 해당 하이퍼파라미터로 재학습시킴

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split, GridSearchCV

# 데이터를 로딩하고 학습 데이터와 테스트 데이터 분리
iris = load_iris()
features = iris.data
label = iris.target
x_train, x_test, y_train, y_test = train_test_split(features, label, test_size=0.2, random_state=121)

dtree = DecisionTreeClassifier()

# 파라미터를 딕셔너리 형태로 설정
parameters = {'max_depth':[1,2,3], 'min_samples_split':[2,3]}

grid_dtree = GridSearchCV(dtree, param_grid=parameters, scoring='accuracy', cv=3) # 디폴트로 refit=True
grid_dtree.fit(x_train, y_train)

# cv=3을 돌면서, 하이퍼파라미터 조합을 다 적용시켜서 그중 최적의 하이퍼파라미터 값
print('GridSearchCV 최적 파라미터:', grid_dtree.best_params_)
# cv=3을 돌면서, 하이퍼파라미터 조합을 다 적용시켜서 그중 최적의 하이퍼파라미터 값의 평가 결과 값
print('GridSearchCV 최고 정확도:', grid_dtree.best_score_)

from sklearn.metrics import accuracy_score
# GridSearchCV의 refit=True로 최적의 하이퍼파라미터로 학습된 estimator 반환
estimator = grid_dtree.best_estimator_

# 최적의 하이퍼파라미터로 학습된 estimator를 이용해 테스트 데이터 세트에 대해 예측 및 평가
pred = estimator.predict(x_test)
print('테스트 데이터 세트 정확도:{0:.4f}'.format(accuracy_score(y_test, pred)))

검증 세트의 최고 정확도 0.975보다 테스트 데이터 세트 정확도가 0.9667로 약간 낮게 나왔다. (검증 세트보다 약간 낮게 나오는게 일반적)→ 정확도 차이가 0.975보다 엄청 작게 나온게 아니므로 과적합이 없다고 판단

 

 

일반적인 머신러닝 모델 적용 방법은 학습 데이터를 GridSearchCV를 이용해 최적 하이퍼파라미터 튜닝을 수행한 뒤에 별도의 테스트 세트에서 이를 평가한다.

('데이터 전처리' 카테고리에서 '데이터 전처리 정리 및 실습'  파트에서 발생한 오류)

# 특징 선택과 모델 하이퍼 파라미터 튜닝
from sklearn.ensemble import RandomForestClassifier as RFC
from xgboost import XGBClassifier as XGB
from lightgbm import LGBMClassifier as LGBM
from sklearn.feature_selection import *
from sklearn.model_selection import ParameterGrid
from sklearn.metrics import f1_score

# 모델 파라미터 그리드 설계
model_parameter_grid = dict()
model_parameter_grid[RFC] = ParameterGrid({'max_depth':[2, 3, 4],
                                          'n_estimators':[50, 100]})
model_parameter_grid[XGB] = ParameterGrid({'max_depth':[2, 3, 4],
                                          'n_estimators':[50, 100],
                                          'learning_rate':[0.05, 0.1, 0.15, 0.2]})
model_parameter_grid[LGBM] = ParameterGrid({'max_depth':[2, 3, 4],
                                           'n_estimators':[50, 100],
                                           'learning_rate':[0.05, 0.1, 0.15, 0.2]})
# 튜닝 시작
best_score = 0
for k in range(30, 5, -1):
    s_x_train = x_train[pvals.iloc[:k].index]
    s_x_test = x_test[pvals.iloc[:k].index]
    for M in model_parameter_grid.keys():
        for P in model_parameter_grid[M]:
            model = M(**P).fit(s_x_train, y_train)
            pred = model.predict(s_x_test)
            score = f1_score(y_test, pred)
            if score > best_score:
                best_score = score
                best_feature = s_x_train.columns
                best_model = M
                best_parameter = P

print(best_score)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [78], in <cell line: 3>()
      6 for M in model_parameter_grid.keys():
      7     for P in model_parameter_grid[M]:
----> 8         model = M(**P).fit(s_x_train, y_train)
      9         pred = model.predict(s_x_test)
     10         score = f1_score(y_test, pred)

File ~\AppData\Roaming\Python\Python39\site-packages\xgboost\core.py:575, in _deprecate_positional_args.<locals>.inner_f(*args, **kwargs)
    573 for k, arg in zip(sig.parameters, args):
    574     kwargs[k] = arg
--> 575 return f(**kwargs)

File ~\AppData\Roaming\Python\Python39\site-packages\xgboost\sklearn.py:1357, in XGBClassifier.fit(self, X, y, sample_weight, base_margin, eval_set, eval_metric, early_stopping_rounds, verbose, xgb_model, sample_weight_eval_set, base_margin_eval_set, feature_weights, callbacks)
   1352     expected_classes = np.arange(self.n_classes_)
   1353 if (
   1354     self.classes_.shape != expected_classes.shape
   1355     or not (self.classes_ == expected_classes).all()
   1356 ):
-> 1357     raise ValueError(
   1358         f"Invalid classes inferred from unique values of `y`.  "
   1359         f"Expected: {expected_classes}, got {self.classes_}"
   1360     )
   1362 params = self.get_xgb_params()
   1364 if callable(self.objective):

ValueError: Invalid classes inferred from unique values of `y`.  Expected: [0 1], got [-1  1]

 

클래스가 0부터 시작해야 하기 때문에 발생합니다(버전 1.3.2부터 필요함)

이를 해결하는 쉬운 방법은 sklearn.preprocessing 라이브러리의 LabelEncoder를 사용하는 것입니다.

 

솔루션

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
y_train = le.fit_transform(y_train)

y_train의 레이블을 새로 인코딩 한 뒤 학습을 하니 해결이 가능했다.

 

이후 모델 평가 시 주의사항

le.inverse_transform으로 인코딩된 값을 다시 디코딩해서 새로 들어오는 데이터 즉, 테스트 데이터의 레이블 값(y_test)의 원본 형태인 -1과 1로 맞추어야 score를 구할 수 있음

# 튜닝 시작
best_score = 0
for k in range(30, 5, -1):
    s_x_train = x_train[pvals.iloc[:k].index]
    s_x_test = x_test[pvals.iloc[:k].index]
    for M in model_parameter_grid.keys():
        for P in model_parameter_grid[M]:
            model = M(**P).fit(s_x_train, y_train)
            pred = model.predict(s_x_test)
            pred = le.inverse_transform(pred)
            score = f1_score(y_test, pred)
            if score > best_score:
                best_score = score
                best_feature = s_x_train.columns
                best_model = M
                best_parameter = P

print(best_score)
print('\n')
print(best_feature)
print('\n')
print(best_model)
print('\n')
print(best_parameter)

 

출처:https://stackoverflow.com/questions/71996617/invalid-classes-inferred-from-unique-values-of-y-expected-0-1-2-3-4-5-got

넘파이
▷ndarray: N차원 배열 객체


▷ndarray 생성
넘파이 모듈의 array() 함수로 생성하며 인자로 주로 파이썬 list를 입력한다.


▷ndarray 형태와 차원

ndarray의 shape는 ndarray.shape 속성으로, 차원은 ndarray.ndim 속성으로 알 수 있다.
ndarray 배열의 shape 변수는 ndarray의 크기, 즉 행과 열의 수를 튜플 형태로 가지고 있으며 이를 통해 ndim을 사용 하지 않고 바로 ndarray 배열의 차원을 알 수 있다.(shape안의 숫자 개수를 통해 차원을 바로 알 수 있음, 리스트의 형태를 보고도 차원을 바로 알 수 있음)


▷ndarray 데이터 타입
- ndarray내의 데이터값은 숫자 값, 문자열 값, 불 값 등이 모두 가능하다.
- ndarray내의 데이터 타입은 그 연산의 특성상 같은 데이터 타입만 가능하다. 즉, 한개의 ndarray 객체에 int와 float이 함께 있을 수 없다.
- 만약 다른 데이터 유형이 섞여 있는 리스트를 ndarray로 변경하면 데이터 크기가 더 큰 데이터 타입으로 형 변환을 일괄 적용한다.
- ndarray내의 데이터 타입은 ndarray.dtype으로 확인한다.


▷ndarray 데이터 타입 변환
- ndarray.astype()을 이용하여 ndarray 내 데이터값의 타입을 변환하고, 변경을 원하는 타입을 astype()에 인자로 입력한다.
- 대용량 데이터를 다룰 시 메모리 절약을 위해서 형변환을 한다.
→파이썬 기반의 머신러닝 알고리즘은 대부분 메모리로 데이터를 전체 로딩한 다음 이를 기반으로 알고리즘을 적용하기 때문에 대용량의 데이터를 로딩할 때는 수행속도가 느려지거나 메모리 부족으로 오류가 발생할 수 있다.
→가령 int형으로 충분한 경우인데, 데이터 타입이 float라면 int형으로 바꿔서 메모리를 절약할 수 있다.


▷ndarray를 편리하게 생성하기-arange, zeros, ones
특정 크기와 차원을 가진 ndarray를 연속값이나 0또는 1로 초기화 생성해야 할 경우 arange(), zeros(), ones()를 이용해 쉽게 ndarray를 생성할 수 있다. 주로 테스트용으로 데이터를 만들거나 데이터를 일괄적으로 초기화해야 할 경우에 사용된다.

●arange()는 array를 range()로 표현한 것이다. default 함수 인자는 stop값이며, 0부터 stop값-1까지의 연속 숫자 값으로 구성된 1차원 ndarray를 만들어 준다. 여기서는 stop값만 부여했으나 range와 유사하게 start값과 step을 활용할 수도 있다.
●zeros()와 ones()는 함수 인자로 튜플 형태의 shape값을 입력하면 모든 값을 0이나 1로 채운 해당 shape를 가진 ndarray를 반환한다. 그리고 dtype을 정해주지 않으면 default로 float64 형의 데이터로 ndarray를 채운다.


▷ndarray의 차원과 크기를 변경하는 reshape()
●reshape()는 ndarray를 특정 차원 및 형태로 변환한다. 변환 형태를 함수 인자로 부여하면 된다.

●reshape(-1,5)와 같이 인자에 -1을 부여하면 -1에 해당하는 axis의 크기는 가변적이되 -1이 아닌 인자값(여기서는 5)에 해당하는 axis 크기는 인자값으로 고정하여 ndarray의 shape를 변환한다.

●reshape()는 reshape(-1,1), reshape(-1,)과 같은 형식으로 변환이 요구되는 경우가 많다. 주로 머신러닝 API의 인자로 1차원 ndarray를 명확하게 2차원 ndarray로 변환하여 입력하기를 원하거나, 또는 반대의 경우가 있을 경우 reshape()를 이용하여 ndarray의 형태를 변환시켜 주는데 사용된다.


★ndarray의 axis 축 이해
강의 'ndarray의 axis 축 이해' 참고(axis는 엄청 헷갈리기 때문에 이론이 생각안나면 다음 강의 참고)


▷ndarray의 데이터 세트 선택하기-인덱싱(Indexing)

1. 단일 값 추출
- 1차원 ndarray
●ndarray는 axis를 기준으로 0부터 시작하는 위치 인덱스값을 가지고 있다. 해당 인덱스 값을 []에 명시하여 단일 값을 추출한다. 마이너스가 인덱스로 사용되면 맨 뒤에서부터 위치를 지정한다.

- 2차원 ndarray
●1차원과 다차원 ndarray에서의 데이터 접근의 차이는 콤마(,)로 분리된 인덱스를 통해 접근하는 것이다. 즉, 2차원의 경우 콤마로 분리된 로우와 컬럼 위치의 인덱스를 통해 접근한다.

2. 슬라이싱(Slicing)
- 1차원 ndarray
●슬라이싱은 :을 이용하여 연속된 값을 선택한다.

- 2차원 ndarray
●2차원 ndarray에서의 슬라이싱도 1차원 ndarray에서의 슬라이싱과 유사하며, 단지 콤마(,)로 로우와 컬럼 인덱스를 지칭하는 부분만 다르다.

3. 팬시 인덱싱
- 1차원 ndarray
●팬시 인덱싱은 리스트나 ndarray로 인덱스 집합을 지정하면 해당 위치의 인덱스에 해당하는 ndarray를 반환하는 인덱싱 방식이다.

- 2차원 ndarray
●위의 인덱싱과 마찬가지로 콤마(,)로 로우와 컬럼 인덱스를 지칭하는 부분만 다르다.

4. 불린 인덱싱
●불린 인덱싱은 조건 필터링과 추출을 동시에 할 수 있기 때문에 매우 자주 사용되는 인덱싱 방식이다.


▷배열의 정렬-sort()와 argsort()
-넘파이 sort() 유형
1. np.sort(ndarray): 인자로 들어온 원 행렬은 그대로 유지한 채 원 행렬의 정렬된 행렬을 반환
2. ndarray.sort(): 원 행렬 자체를 정렬한 형태로 반환하며 반환 값은 None
np.sort()나 ndarray.sort() 모두 기본적으로 오름차순으로 행렬 내 원소를 정렬한다. 내림차순으로 정렬하기 위해서는 [::-1]을 적용한다. np.sort()[::-1]과 같이 사용하면 된다.
⊙배열이 2차원 이상일 경우에 axis 축 값 설정을 통해 로우 방향, 또는 컬럼 방향으로 정렬을 수행할 수 있다.

-argsort()
원본 행렬 정렬 시 정렬된 행렬의 원래 인덱스를 필요로 할 때 np.argsort()를 이용한다. np.argsort()는 정렬 행렬의 원본 행렬 인덱스를 ndarray 형으로 반환한다.
argsort()는 넘파이의 데이터 추출에서 많이 사용되므로 주의 깊게 봐야한다.


▷선형대수 연산-행렬 내적(행렬 곱)
행렬 내적의 특성으로 왼쪽 행렬의 열 개수와 오른쪽 행렬의 행 개수가 동일해야 내적 연산이 가능하다.


▷선형대수 연산-전치 행렬
원 행렬에서 행과 열 위치를 교환한 원소로 구성한 행렬을 그 행렬의 전치 행렬이라고 한다.

numpy.ipynb
0.02MB

판다스
▷판다스의 주요 구성 요소-DataFrame, Series, Index


▷DataFrame과 리스트, 딕셔너리, 넘파이 ndarray 상호 변환


▷DataFrame의 컬럼 데이터 세트 생성과 수정
→ [ ]연산자를 이용해 쉽게 할 수 있다.
●DataFrame[ ] 내에 새로운 컬럼명을 입력하고 값을 할당해주기만 하면 된다.
ex1 ) titanic_df['Age_0'] = 0 을 하면 새로운 컬럼명 'Age_0'에 모든 데이터값이 0으로 할당된다.
ex2 ) titanic_df['Age_by_10'] = titanic_df['Age'] * 10 을 하면 새로운 컬럼명 'Age_by_10'에 기존 컬럼 'Age'에 10을 곱한 값들이 할당된다.
ex3 ) titanic_df['Family_No'] = titanic_df['SibSp'] + titanic_df['Parch'] + 1 도 마찬가지다.
●DataFrame 내의 기존 컬럼 값도 쉽게 일괄적으로 업데이트할 수 있다.
ex1 ) titanic_df['Age_by_10'] = titanic_df['Age_by_10'] + 100


▷(TIP). 파라미터 inplace
변수 갱신 여부를 설정하는 파라미터
(본인은 변수를 다시 정의하는 것보다 inplace를 사용하는게 편하더라)


▷DataFrame 데이터 삭제
DataFrame의 drop()
DataFrame.drop(labels=None, axis=0, index=None, columns=None, level=None, inplace=False, errors='raise')
●axis: DataFrame의 로우를 삭제할 때는 axis=0, 컬럼을 삭제할 때는 axis=1로 설정(2차원 행렬에서 axis0은 로우 방향, axis1은 컬럼 방향: 'numpy ndarray axis 축 이해' 참고)
●로우를 삭제할 때는 labels에 index를 입력, 컬럼을 삭제할 때는 labels에 컬럼명을 입력
●여러개의 로우나 컬럼들의 삭제는 drop의 인자로 삭제 로우나 컬럼들을 리스트로 입력
●원본 DataFrame은 유지하고 드롭된 DataFrame을 새롭게 객체 변수로 받고 싶다면 inplace=False로 설정(디폴트 값이 False임)
ex ) titanic_drop_df = titanic_df.drop('Age_0', axis=1, inplace=False)
●원본 DataFrame에 드롭된 결과를 적용할 경우에는 inplace=True를 적용
ex ) titanic_df.drop('Age_0', axis=1, inplace=True)


▷판다스 Index 객체
●판다스의 Index 객체는 RDBMS의 PK(Primary Key)와 유사하게 DataFrame, Series의 레코드를 고유하게 식별하는 객체(하지만 판다스 Index는 별도의 컬럼값이 아니다)
●DataFrame/Series 객체는 Index 객체를 포함하지만 DataFrame/Series 객체에 연산 함수를 적용할 때 Index는 연산에서 제외된다. Index는 오직 식별용으로만 사용된다.
●DataFrame/Series에서 Index 객체만 추출하려면 DataFrame.index 또는 Series.index 속성을 통해 가능
●판다스 Index는 반드시 숫자형 값이 아니어도 되며, 고유한 값을 유지 할 수 있다면 문자형/Datetime도 상관 없음
●DataFrame 및 Series에 reset_index() 메서드를 수행하면 새롭게 인덱스를 연속 숫자 형으로 할당하며 기존 인덱스는 'index'라는 새로운 컬럼명으로 추가된다.
# reset_index(drop=False, inplace=False)
(TIP)'index'가 새로운 컬럼명으로 추가 안하려면 파라미터 drop=True를 사용함


▷DataFrame 인덱싱 및 슬라이싱
1. [ ]
데이터 프레임의 컬럼 선택:df[col_name] or df[col_name_list]
ex) df[’col1’] →Series 반환 / df[[’col1’]] →데이터 프레임 반환
즉, column name => Series / column name list => Data Frame
컬럼 필터링 용도로만 사용!!

2. loc[ ], iloc[ ]
→위치 기반 인덱싱:iloc / 명칭(레이블) 기반 기반 인덱싱:loc

iloc[0,1] = 3 / loc[0, 'PassengerID'] = 1
iloc[0,1] = 3 / loc[0, 'PassengerID'] = error발생

3. 불린 인덱싱(마스킹 검색)
조건식에 따른 필터링을 제공(조건이 여러개면 각 조건마다 괄호()를 씌움)
df[부울리스트] / df.loc[부울 리스트] : True인 요소의 위치에 대응되는 행만 가져옴
ex ) titanic_boolean = titanic_df[titanic_df['Age'] > 60] / titanic_boolean = titanic_df.loc[titanic_df['Age'] > 60]
→둘 중 입맛에 맞는거 사용
# 불린 인덱싱은 [ ], loc[ ]에서 공통으로 지원한다. iloc[ ]는 불린 인덱싱이 지원되지 않는다.
# and 조건: & / or 조건: | / not 조건: ~


▷DataFrame과 Series의 정렬-sort_values()
●DataFrame의 sort_values() 메소드는 by 인자로 정렬하고자 하는 컬럼값을 입력 받아서 해당 컬럼값으로 DataFrame을 정렬, 오름차순 정렬이 기본이며 ascending=True로 설정됨. 내림차순 정렬 시 ascending=False로 설정

마찬가지로, 여러 개의 컬럼으로 정렬하는 경우도 by 인자로 정렬하고자 하는 컬럼값들을 입력

●Series의 경우 컬럼 한 개에 대한 값이므로 by 인자를 사용할 필요가 없고, 오름차순/내림차순 정렬 기준만 설정하면 된다.
→Series.sort_values()


▷Aggregation 함수 적용
●sum(), max(), min(), count() 등은 DataFrame/Series에서 집합(Aggregation)연산을 수행
●DataFrame의 경우 DataFrame에서 바로 aggregation을 호출할 경우 모든 컬럼에 해당 aggregation을 적용
ex ) titanic_df.count()
●특정 컬럼에 aggregation을 적용하려면 컬럼 필터링 후 aggregation 적용
ex ) titanic_df[['Age','Fare']].mean()


▷groupby()적용
●DataFrame을 groupby 대상 컬럼을 기준으로 그룹화하는 함수
●사용 구조:df.groupby(groupby 대상 컬럼)[적용 기준 컬럼].집계함수
●주요 입력
- by: groupby 대상 컬럼
- as_index: groupby 대상 컬럼을 인덱스로 사용할 것인지 여부(default:True)

1. groupby 대상 컬럼을 제외한 모든 컬럼에 해당 aggregation 함수 적용
df.groupby('Pclass').count()
2. groupby에 특정 컬럼만 aggregation 함수 적용
df.groupby('Pclass')[['PassengerID', 'Survived']].count()
3. 서로 다른 aggregation 함수 적용
df.groupby('Pclass')['Age'].agg([max, min])
4. 여러 개의 컬럼이 서로 다른 aggregation 함수 적용(딕셔너리 형태로 aggregation이 적용될 컬럼들과 aggregation 함수를 입력)
agg_format = {'Age': 'max', 'SibSp': 'sum', 'Fare': 'mean'}
df.groupby('Pclass').agg(agg_format)

(TIP). groupby 대상 컬럼이 여러개이면 리스트에 여러개의 컬럼들을 넣고 입력(어떤 경우에 리스트로 만들고 안만들고 헷갈리니 컬럼이 한개일때도 리스트에 컬럼을 넣고 입력->groupby가 아닌 메소드일때도!!)
위에는 안한 경우임


▷결측 데이터 처리하기
●isna()로 결측 데이터 여부 확인
→isna().sum() 을 사용해 결측 데이터의 개수 확인
●fillna()로 결측 데이터 대체


▷nunique()
유니크한 값의 개수 반환
ex) df['Survived'].nunique()


▷replace()
원본 값을 특정값으로 대체
●한 개의 값만 대체할 경우
df['Sex'].replace('male', 'Man')
●여러개의 값을 대체할 경우 딕셔너리 사용
df['Sex'].replace({'male': 'Man', 'female': 'Woman'})
# 파라미터 inplace 활용



▷apply lambda 식으로 데이터 가공
●lambda 식 이해
lambda 식은 파이썬에서 함수형 프로그래밍을 지원하기 위해 만들어졌으며, 일반적으로 파이썬에서 함수를 만들 때 여러 줄의 코드를 찍지만 lambda식을 사용하면 한줄의 코드로 함수를 만들수 있다.

●Pandas는 apply lambda 식을 결합해 DataFrame이나 Series의 레코드별(행별)로 데이터를 가공하는 기능을 제공한다. 판다스의 경우 컬럼에 일괄적으로 데이터 가공을 하는 것이 속도 면에서 더 빠르나 복잡한 데이터 가공이 필요할 경우 어쩔 수 없이 apply lambda를 이용한다.

lambda 식을 이용할 때 여러 개의 값을 입력 인자로 사용해야 할 경우 map()함수를 결합해서 사용한다.

a = [1,2,3]
squares = map(lambda x : x**2, a)
list(squares)

lambda 식은 if, else만 지원하고 elif는 지원하지 않는다. if 절의 경우 if 식보다 반환 값을 먼저 기술해야 한다. 이는 lambda 식 ':' 기호의 오른편에 반환 값이 있어야 하기 때문이다.

elif를 이용하기 위해서는 else 절을 ()로 내포해 () 내에서 다시 if else 적용해 사용한다.

titanic_df['Age_cat'] = titanic_df['Age'].apply(lambda x : 'Child' if x<=15 else ('Adult' if x <= 60 else 
                                                                                  'Elderly'))

하지만 else if가 많이 나와야 하는 경우 이렇게 else를 계속 내포해서 쓰기 부담스럽다. 이 경우 아예 별도의 함수를 만들어 반환값으로 지정하는 것이 더 나을 수 있다.

def get_category(age):
    if age <= 5:
        cat = 'Baby'
    elif age <= 12:
        cat = 'Child'
    elif age <= 18:
        cat = 'Teenager'
    elif age <= 25:
        cat = 'Student'
    elif age <= 35:
        cat = 'Young Adult'
    elif age <= 60:
        cat = 'Adult'
    else:
        cat = 'Elderly'

    return cat

# lambda 식에 위에서 생성한 get_category( ) 함수를 반환값으로 지정. 
titanic_df['Age_cat'] = titanic_df['Age'].apply(lambda x : get_category(x))
titanic_df[['Age', 'Age_cat']].head()
pandas.ipynb
1.15MB

실제 프로세스에서는 데이터 탐색과 전처리 사이에 피드백 루프가 존재한다.

즉, 탐색을 한 번에 다하고 전처리를 한 번에 다하는 것이 아니다.

그리고 전처리에도 다양한 하이퍼 파라미터가 있기에 모델의 하이퍼 파라미터와 같이 튜닝해야 한다.

 

현실 지도학습 모델 개발 프로세스 (1) 필수 전처리

일반적으로 필수 전처리는 특별한 튜닝이 필요하지 않으므로 순차적으로 진행한다.

필수 전처리

현실 지도학습 모델 개발 프로세스 (2) 성능 향상을 위한 전처리

일반적으로 성능 향상을 위한 전처리는 튜닝도 같이 수행해야 한다.

성능 향상을 위한 전처리

특징 선택 단계에서 차원을 축소하므로 신규 특징 추가에서 특징을 다수 추가하는 것이 최종적으로 성능 향상에 도움이 될 가능성이 높고 이전 스텝으로 돌아가는 피드백 루프를 줄일 수 있다.(파생변수 생성)

 

이상치 확인 단계와 특징 간 상관관계 확인 단계는 많이 스킵하는 경우도 있다. 왜냐하면 특징 간 상관관계 확인 단계는  상관성이 있을 때 영향을 받는 모델이 있고 영향을 받지 않는 모델이 있다. 즉, 어떤 모델을 선택하느냐에 따라서 상관성을 무시하는 경우도 존재한다. 그리고 이상치 확인 단계는 이상치를 제거하는 것이 유리한 상황일 수도 있고 그렇지 않은 상황이 있을 수 있기 때문이다.(도메인 지식에 의해 이상치가 필요한 분석인 경우)

 

왜도 확인 단계도 마찬가지로 스킵하는 경우가 종종 있다.

 

별이 붙은 작업은 어떻게 튜닝하느냐에 따라서 성능 차이가 발생할 수 있다. 그래서 별이 붙은 단계는 튜닝을 하는 것이 중요하다.

성능 향상을 위한 전처리

스케일링을 먼저 한 이유는 재샘플링은 거의 다 거리 기반의 방법론이기 때문에 실제로는 스케일링을 먼저 해주고 재샘플링을 하는 것이 더 좋은 효과를 보일 수 밖에 없다. 

 

모델 목록 정의 즉, 모델 파라미터 튜닝은 튜닝 작업 중 제일 중요하다.

 

상황에 따라서 더 많은 프로세스가 포함될 수 있다.

 

 

파라미터 그리드 설계(실제로 튜닝하는 범위)

EDA(탐색)를 제대로 하지 않으면 파라미터 그리드에 포함된 파라미터 개수가 수십~수백만개도 된다.

다시 말해, EDA를 해서 어떤 튜닝 작업을 해야할지 정하는 것이 굉장히 중요하다.

 


(Chapter 22) 01. 최종실습예제 연습.ipynb
0.07MB

차원의 저주란?

차원(특징 수)이 증가함에 따라 필요한 데이터의 양과 시간 복잡도가 기하급수적으로 증가하는 문제를 의미한다.

차원이 증가함에 따라서 각 공간에 들어가는 샘플이 적어질 수 밖에 없기 때문에 정확한 판단을 위해서는 샘플이 더 많아질 수 밖에 없다. 그리고 샘플이 더 많게 하기 위해서는 데이터를 더 수집해야 하기 때문에 쉬운 문제가 아니다. 그래서 샘플이 부족하기 때문에 과적합으로 이어질 것이다. 특징이 범주형인 경우에 비해서 연속형인 경우 특징이 늘면 공간이 더 세분화되고 더 늘어날 수 있어서 단순히 특징이 많고 적고도 고려해야하고 특징의 종류도 고려해야한다.

 

●차원을 줄여야 하는 이유(특징 개수를 줄여야 하는 이유)

1. 차원이 증가함에 따라 모델 학습 시간이 정비례하게 증가한다.

2. 차원이 증가하면 각 결정 공간에 포함되는 샘플 수가 적어져, 과적합으로 이어져 성능 저하가 발생할 수 있다.

.

특징이 너무 적으면 과소적합이 일어날 수 있고, 특징이 늘어날수록 모델 성능이 올라가다가 어느 순간에 떨어질 것이다. 이 떨어지는 구간이 과적합이 발생한 구간이다.

 

차원의 저주 해결방법: 특징 선택

특징 선택은 분류 및 예측에 효과적인 특징만 선택하여 차원을 축소하는 방법이다. 구체적으로 설명하자면 n개의 특징으로 구성된 특징 집합 {x1, x2, ..., xn}에서 m < n개의 특징을 선택하여, 새로운 특징 집합 {x'1, x'2, ..., x'm} ⊂ {x1, x2, ..., xn}을 구성하는 방법이다.

특징 선택을 할 때 많이 하는 오해가 있는데 특징 선택을 할 때는 특징이 많은 데이터에만 정의해야 한다고 오해하는 것이다. 즉, 특징 선택은 특징이 많은 데이터에만 적용해야 하는 것은 아니다. 왜냐하면 특징이 적더라도 특징 선택을 했을 때 모델의 성능이 좋아지는 경우가 존재하기 때문이다.

 

## 코드 실습 ##

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("appendicitis.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Class', axis=1)
Y = df['Class']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 특징에 따른 SVM 모델 테스트 함수 작성
from sklearn.svm import SVC
from sklearn.metrics import f1_score

def feature_test(x_train, x_test, y_train, y_test, features):
    s_x_train = x_train[features]
    s_x_test = x_test[features]
    
    model = SVC().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    return f1_score(y_test, pred_y)

base_score = feature_test(x_train, x_test, y_train, y_test, x_train.columns) # 모든 특징을 썼을 때의 점수
print(base_score)

import itertools
c_list = list(range(1, len(x_train.columns))) # base_score: 모든 특징을 사용한 경우는 제외 
outperform_ratio_list = []
best_score = 0

for c in c_list: # c는 선택한 특징 개수
    print(c)
    c_num = 0 # 특징을 c개 뽑았을 때, 모든 특징을 썼을 때보다 성능이 좋은 경우
    c_dem = 0 # 특징을 c개 뽑는 경우의 수
    
    # itertools.combinations(p,r): 이터레이터 객체 p에서 크기 r의 가능한 모든 조합을 갖는 이터레이터를 생성
    for features in itertools.combinations(x_train.columns, c):
        score = feature_test(x_train, x_test, y_train, y_test, list(features)) # itertools는 tuple 형태로 값을 반환해서 형변환을 해줌
        if score > best_score:
            best_score = score
            best_feature = list(features)
            
        if score > base_score:
            c_num += 1
        
        c_dem += 1
        
    outperform_ratio_list.append(c_num / c_dem)
    # 예를 들어, 특징 개수를 한 개 뽑는다면 6개의 combination이 있는데
    # 6개의 combination의 score값이 base_score보다 큰 경우 c_num을 1씩 더해서 업데이트
    # c_dem은 combination의 값과 동일: 특징 개수를 한 개 뽑는다면 경우의 수가 6개

%matplotlib inline
from matplotlib import pyplot as plt
plt.bar(c_list, outperform_ratio_list)

모든 특징을 썼을 때보다 성능이 좋았던 특징 집합의 비율

best_feature, best_score

위의 코드는 특징이 7개인 데이터에 대해, 모든 특징 집합을 비교해 본 결과이다.

모든 특징을 사용했을 때의 성능 f1-score는 0.2857이지만 특징을 [At1, At4] 두 개만 쓰는 경우 0.7499로 가장 좋은 성능을 확인 할 수 있다.

이런 주먹구구식 특징 선택은 선택 가능한 모든 특징 집합을 비교/평가하여 가장 좋은 특징 집합을 선택할 수 있지만 특징 개수가 많아지면 현실적으로 불가능하다. 즉, 최적의 결과를 보장할 수 있지만 최적의 결과를 얻기까지 시간이 너무 오래걸린다. 

→특징 개수가 n개라면, 2^n-1번의 모형 학습이 필요하므로, 현실적으로 적용 불가능하다. 1초에 1억 번의 모형을 학습할 수 있는 슈퍼 컴퓨터가, 1000개의 특징이 있는 데이터에 대해, 이 방법을 적용하여 가장 좋은 특징 집합을 선택하는데 소요되는 시간은 400조년이다.

 

이런 문제점을 해결하기 위한 방법을 아래에서 볼 것이다.

 

클래스 관련성 기반의 특징 선택

특징과 라벨이 얼마나 관련이 있는지를 나타내는 클래스 관련성이 높은 특징을 우선 선택하는 방법이다.

클래스 관련성은 한 특징이 클래스를 얼마나 잘 설명하는지를 나타내는 척도로 특징과 라벨 간 독립성을 나타내는 통계량상관계수, 카이제곱 통계량, 상호정보량 등을 사용하여 측정한다.(서로 독립적이면 특징은 라벨을 예측하는데 도움이 안될 것이고 서로 종속적이면 특징은 라벨을 예측하는데 도움이 될 것이다.)

즉, 클래스 관련성이 높은 특징은 분류 및 예측에 도움이 되는 특징이며, 그렇지 않은 특징은 도움이 되지 않는 특징이다.

 

예를 들어, 특징이 범주형이고 라벨도 범주형인 상황(범주형 특징-분류)

x1이 1이면 y는 90% 세모라고 분류를 해 줄 것이고, x1이 0이면 y는 90% 동그라미라고 분류를 해 줄 것이다. 원래 데이터의 분포가 5대5였는데, x1이 1이면 y가 세모일 확률이 높아지고, x1이 0이면 y가 동그라미일 확률이 높아지는 것을  보고 x1과 y가 크게 관련이 있다는 것을 알 수 있다.

반대로 x2를 보면 x2가 1이거나 0이거나 관계없이 둘 다 5대5로 똑같다. 다시 말해 원래 데이터의 분포도 5대5(동그라미 100개, 세모 100개)였는데, x2가 주어지던 안주어지던 상관이 없다. 그러면 x2를 알고 있으나 모르고 있으나 크게 도움이 안되니까 x2는 라벨을 분류하는데 도움이 안되는 특징이다.

 

클래스 관련성 척도 중 대표적으로 많이 쓰는 F-통계량을 살펴보겠다.

F-통계량은 일원분산분석(ANOVA)에서 사용하는 통계량으로, 집단 간 평균 차이가 있는지를 측정하기 위한 통계량이다.

x1의 값에 따라 y 분류가 어느정도 가능하지만, x2는 그렇지 않다.

 

★클래스 관련성 척도 분류

(범주형 변수를 더미화 했기 때문에 특징 유형이 이진형이다.)

●관련 함수: sklearn.feature_selection.SelectKBest

-주요 입력

▷scoring_func: 클래스 관련성 측정 함수(예: chi2, mutual_info_classif 등)

▷k: 선택하는 특징 개수

 

-주요 메서드

▷.fit, .transform, .fit_transform: 특징을 선택하는데 사용하는 메서드

▷.get_support(): 선택된 특징의 인덱스를 반환

 

-주요 속성

▷scores_: scoring_func으로 측정한 특징별 점수

▷pvalues_: scoring_func으로 측정한 특징별 p-value(1에 가까울수록 독립적이며, 0에 가까울수록 관련성이 높음→작을수록 좋은 특징)

→score자체를 보는 경우는 드물고 pvalue를 바탕으로 독립적인지 아닌지를 측정한다. pvalue를 보는 경우는 k값을 따로 설정하지 않고 pvalue가 얼마 미만인 경우만 고르는 경우라던가 특징 유형이 다른 경우에 관련 함수가 달라지기 때문에 그 때 통계량을 비교하기 어려우니 pvalue로 비교한다. 

특징 유형이 다른 경우 pvalue로 비교하는 방법 말고 다른 방법은 상호 정보량을 이용한다.


## 코드 실습 ##

●모든 특징 유형이 같은 경우의 특징 선택

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("Sonar_Mines_Rocks.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Y', axis=1)
Y = df['Y']
# 모든 특징 연속형
for i in X.columns:
    print(i, len(df[i].unique()))
Band1 65
Band2 83
Band3 90
Band4 92
Band5 112
Band6 134
Band7 133
Band8 139
Band9 154
Band10 162
Band11 164
Band12 168
Band13 168
Band14 169
Band15 176
Band16 179
Band17 177
Band18 175
Band19 173
Band20 179
Band21 187
Band22 183
Band23 173
Band24 175
Band25 181
Band26 171
Band27 173
Band28 168
Band29 179
Band30 182
Band31 190
Band32 184
Band33 185
Band34 184
Band35 188
Band36 188
Band37 180
Band38 172
Band39 166
Band40 183
Band41 175
Band42 172
Band43 173
Band44 154
Band45 162
Band46 151
Band47 147
Band48 131
Band49 98
Band50 50
Band51 45
Band52 39
Band53 32
Band54 31
Band55 29
Band56 27
Band57 27
Band58 28
Band59 29
Band60 24
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 라벨이 문자이니 변환
y_train.replace({"M":-1, "R":1}, inplace=True)
y_test.replace({"M":-1, "R":1}, inplace=True)
x_train.shape

# 특징 선택 전 성능 확인(특징이 연속형이고 분류 문제)
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import f1_score

model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)
print(f1_score(y_test, pred_y))

# 특징 선택 수행
from sklearn.feature_selection import *

selector = SelectKBest(f_classif, k = 30)
selector.fit(x_train, y_train)
selected_features = x_train.columns[selector.get_support()]

s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
# transform을 해도되지만 get_support()를 통해 추출한 특징을 바로 슬라이싱 가능
# s_x_train = x_train[selected_features], s_x_test = x_test[selected_features]
# 특징 선택 후 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
print(f1_score(y_test, pred_y))

# 특징 개수 튜닝
best_score = 0

for k in range(5, 50, 5):
    selector = SelectKBest(f_classif, k = k)
    selector.fit(x_train, y_train)
    selected_features = x_train.columns[selector.get_support()]
    
    s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
    s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
    
    model = KNN().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    score = f1_score(y_test, pred_y)
    if score > best_score:
        best_score = score
        best_k = k
    
print(best_score, best_k)

 

 

●특징 유형이 다른 경우의 특징 선택

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("australian.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Class', axis=1)
Y = df['Class']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.shape

# 특징 선택 전 성능 확인
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import f1_score

model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)
print(f1_score(y_test, pred_y))

데이터 설명서를 보면 연속형 특징과 범주형 특징이 섞여있고 분류 문제이다.

# 유니크한 값의 개수를 바탕으로 연속형과 범주형 변수 구분
# type과 상태공간을 확인해야하지만 3을 기준으로 연속형인지 범주형인지 구분 할 수 있다고 가정
continuous_cols = [col for col in x_train.columns if len(x_train[col].unique()) > 3]
category_cols = [col for col in x_train.columns if len(x_train[col].unique()) <= 3]

이 데이터는 분류 문제이기 때문에 범주형 특징에 대해서는 카이제곱 통계량이 적절하고, 연속형 특징에 대해서는 F-통계량이 적절하다. 카이제곱 통계량과 F-통계량을 통계량 기준으로 구분하기 쉽지 않기 때문에 pvalue만 계산해서 pvalue가 작은 것 순서대로 선택을 할 것이다.

# 연속형 변수에 대해서는 f_classif, 범주형 변수에 대해서는 chi2를 적용
from sklearn.feature_selection import *

# scoring_func(x, y)의 결과 -> (statistics, p-value)
continuous_cols_pvals = f_classif(x_train[continuous_cols], y_train)[1]
category_cols_pvals = chi2(x_train[category_cols], y_train)[1]
print(continuous_cols_pvals)
print('\n')
print(category_cols_pvals)

# 각각을 Series로 변환(value: pvalue, index: column name)
continuous_pvals_s = pd.Series(continuous_cols_pvals, index = continuous_cols)
category_pvals_s = pd.Series(category_cols_pvals, index = category_cols)
continuous_pvals_s

category_pvals_s

# 두 Series를 합친 뒤, 오름차순으로 정렬
pvals = pd.concat([continuous_pvals_s, category_pvals_s])
pvals.sort_values(ascending=True, inplace=True)
pvals
# pvalue가 작은 것 순서대로 선택(앞에 나오는 특징부터 좋은 특징)

# 특징 선택
k = 10

s_x_train = x_train[pvals.iloc[:k].index]
s_x_test = x_test[pvals.iloc[:k].index]
# 특징 선택 후 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
print(f1_score(y_test, pred_y))

# 특징 개수 튜닝
best_score = 0

for k in range(1, 14, 1):
    s_x_train = x_train[pvals.iloc[:k].index]
    s_x_test = x_test[pvals.iloc[:k].index]
    
    model = KNN().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    score = f1_score(y_test, pred_y)
    if score > best_score:
        best_score = score
        best_k = k

print(best_score, best_k)

연속형 변수와 범주형 변수가 섞여있을 때 위의 과정은 좀 귀찮고 편하게 하고 싶으면 상호 정보량을 이용한다.

# 상호 정보량을 이용한 특징 선택 수행
selector = SelectKBest(mutual_info_classif, k = 10)
selector.fit(x_train, y_train)
selected_features = x_train.columns[selector.get_support()]

s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
# 상호 정보량을 이용한 특징 선택 후 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
print(f1_score(y_test, pred_y))

# 특징 개수 튜닝
best_score = 0

for k in range(1, 14, 1):
    selector = SelectKBest(mutual_info_classif, k = k)
    selector.fit(x_train, y_train)
    selected_features = x_train.columns[selector.get_support()]
    
    s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
    s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
    
    model = KNN().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    score = f1_score(y_test, pred_y)
    if score > best_score:
        best_score = score
        best_k = k
        
print(best_score, best_k)


참고자료https://datascienceschool.net/03%20machine%20learning/14.03%20%ED%8A%B9%EC%A7%95%20%EC%84%A0%ED%83%9D.html

 

특징 선택 — 데이터 사이언스 스쿨

.ipynb .pdf to have style consistency -->

datascienceschool.net

의사결정나무, 랜덤포레스트 등 모델 기반 특징을 선택하는 방법도 있다는 것을 알게됨

[학습자료]

패스트캠퍼스 “파이썬을 활용한 데이터 전처리 Level UP 올인원 패키지 Online.”

 

확률 모델 비용 민감 모델 구축하는 과정에서 cut off value 값에 따른 재현율과 정밀도 변화를 확인하는 과정에서의 오류

 

일단, 로지스틱 회귀모델을 만들고 cut off value를 조정하는 함수를 작성했다.

# cut off value를 조정하는 함수 작성
def cost_sensitive_model(model, cut_off_value, x_test, y_test):
    probs = model.predict(x_test)
    probs = pd.DataFrame(probs, columns = model.classes_)
    pred_y = 2 * (probs.iloc[:, -1] > cut_off_value) -1
    recall = recall_score(y_test, pred_y)
    accuracy = accuracy_score(y_test, pred_y)
    return recall, accuracy

그 후, cut off value에 따른 재현율과 정확도의 변화를 확인하기 위해 plot을 찍는 과정에서 오류가 발생했다.

# cut off value에 따른 recall과 accuracy 변화 확인
from matplotlib import pyplot as plt
import numpy as np

cut_off_value_list = np.linspace(0, 1, 101)
recall_list = []
accuracy_list = []

for c in cut_off_value_list:
    recall, accuracy = cost_sensitive_model(model, c, x_test, y_test)
    recall_list.append(recall)
    accuracy_list.append(accuracy)

%matplotlib inline
plt.plot(cut_off_value_list, recall_list, label = 'recall')
plt.plot(cut_off_value_list, accuray_list, label = 'accuracy')
plt.legend()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [38], in <cell line: 9>()
      7 accuracy_list = []
      9 for c in cut_off_value_list:
---> 10     recall, accuracy = cost_sensitive_model(model, c, x_test, y_test)
     11     recall_list.append(recall)
     12     accuracy_list.append(accuracy)

Input In [37], in cost_sensitive_model(model, cut_off_value, x_test, y_test)
      2 def cost_sensitive_model(model, cut_off_value, x_test, y_test):
      3     probs = model.predict(x_test)
----> 4     probs = pd.DataFrame(probs, columns = model.classes_)
      5     pred_y = 2 * (probs.iloc[:, -1] > cut_off_value) -1
      6     recall = recall_score(y_test, pred_y)

File ~\anaconda3\lib\site-packages\pandas\core\frame.py:694, in DataFrame.__init__(self, data, index, columns, dtype, copy)
    684         mgr = dict_to_mgr(
    685             # error: Item "ndarray" of "Union[ndarray, Series, Index]" has no
    686             # attribute "name"
   (...)
    691             typ=manager,
    692         )
    693     else:
--> 694         mgr = ndarray_to_mgr(
    695             data,
    696             index,
    697             columns,
    698             dtype=dtype,
    699             copy=copy,
    700             typ=manager,
    701         )
    703 # For data is list-like, or Iterable (will consume into list)
    704 elif is_list_like(data):

File ~\anaconda3\lib\site-packages\pandas\core\internals\construction.py:351, in ndarray_to_mgr(values, index, columns, dtype, copy, typ)
    346 # _prep_ndarray ensures that values.ndim == 2 at this point
    347 index, columns = _get_axes(
    348     values.shape[0], values.shape[1], index=index, columns=columns
    349 )
--> 351 _check_values_indices_shape_match(values, index, columns)
    353 if typ == "array":
    355     if issubclass(values.dtype.type, str):

File ~\anaconda3\lib\site-packages\pandas\core\internals\construction.py:422, in _check_values_indices_shape_match(values, index, columns)
    420 passed = values.shape
    421 implied = (len(index), len(columns))
--> 422 raise ValueError(f"Shape of passed values is {passed}, indices imply {implied}")

ValueError: Shape of passed values is (392, 1), indices imply (392, 2)

​

함수를 정의한 코드를 자세히 보니 실수를 했었다...

cut off value를 활용하기 위해서는 predict 예측값이 아닌 predict_proba로 예측확률을 봤어야 했는데 predict로 코드를 짜서 shape가 맞질 않았던 것이다.

predict_proba를 쓰면 2차원 배열에 각 클래스를 예측한 확률로 나와서 shape가 (392, 2)로 나오는데  predict를 쓰면 1차원 배열에 클래스를 예측한 값 즉, -1과 1이 나오기 때문에 shape가 (392, 1)로 나와서 작성했던 함수에 'cut off value에 따른 recall과 accuracy 변화 확인' 코드가 먹히지 않았던 것이다. 

다시말해, 만든 함수는 predict를 사용해서 1차원 배열이지만, 함수 작성 후 내가 쓴 코드는 2차원 배열꼴을 넣어서 shape가 맞지 않아 오류가 발생했던 것이었다.

 

코드를 쓸 때 나타나는 사소한 실수지만 뭐가 잘못된지 모르고 10분 이상을 코드를 재확인했다... 발견해서 다행 :)

 

 

cut off value를 조정하는 함수 작성 수정 코드

# cut off value를 조정하는 함수 작성
def cost_sensitive_model(model, cut_off_value, x_test, y_test):
    probs = model.predict_proba(x_test)
    probs = pd.DataFrame(probs, columns = model.classes_)
    pred_y = 2 * (probs.iloc[:, -1] >= cut_off_value) -1
    recall = recall_score(y_test, pred_y)
    accuracy = accuracy_score(y_test, pred_y)
    return recall, accuracy

2번째 줄에 predict를 predict_proba로 수정해서 문제 해결 완료~

'Errors' 카테고리의 다른 글

xgboost 지도학습 중 발생한 error  (0) 2023.01.21

비용 민감 모델은 학습 목적식이 정확도를 최대화하는 것을 방지하기 위한 방법이다.

→클래스 불균형 발생 원인 두번째 case를 처리하기 위한 방법

 

비용 민감 모델이란?

일반적으로 학습 목적식 같은 경우 위음성 비용과 위양성 비용을 같다고 본다. 즉, 위음성 비용과 위양성 비용을 구분하지 않는다. 하지만 위음성 비용(암환자의 생명)이 위양성 비용(정상인의 돈과 시간)보다 훨씬 크다. 

→비용 민감 모델은 학습 목적식에서 위음성 비용과 위양성 비용을 다르게 설정하는 모델로, 보통 위음성 비용(소수클래스, 우리의 관심 대상이므로 긍정 클래스)을 위양성 비용(다수클래스, 우리의 관심 대상이 아니므로 부정 클래스)보다 크게 설정한다.

위음성 비용을 위양성 비용보다 w배 크게 설정하는데 w는 1보다 커야한다. w가 1이면 일반적인 모델과 같고, w가 1보다 작으면 부정 클래스에 더 초점을 두는 모델이 된다.

즉, 위음성 비용 = w x 위양성 비용(w > 1)로 설정한 모델을 비용 민감 모델이라고 한다.


비용 민감 모델을 구현할 때 확률 모델과 비확률 모델로 구분해서 알아볼 것이다.

 

확률 모델

확률 모델은 학습 목적식 자체를 바꾸는 것이 아니고 학습의 결과를 바꾸는 방식이다. 로지스틱 회귀, 나이브 베이즈 등의 확률 모델들은 cut-off value, c를 조정하는 방식으로 비용 민감 모델을 구현할 수 있다.

x에 대한 y의 확률이 c보다 크거나 같으면 긍정으로 보고, c보다 작으면 부정으로 본다. 그리고 c를 처음 설정할 때는 보통 0.5로 둔다. 예를 들어, y가 1과 -1이 있을 때 x에 대한 y가 1일 확률이 0.5 이상이면 1로 볼테고 0.5 미만이면 -1로 본다.

 

c값을 작게 둘수록 긍정 클래스로 분류하는 경우가 많아질 것이다. 즉, 긍정 클래스에 대한 결정 공간이 넓어진다는 소리이다. 그렇기 때문에 c값을 0.5보다 작게 설정한다.

→ 우리의 관심 대상인 긍정 클래스 비율이 커질 것이다.

위 그래프와 같이 c값이 작을수록 재현율이 커질테고 클수록 정확도가 커질것이고 그 반대의 경우도 마찬가지로 성립한다.

 

그리고 정확한 확률 추정은 불가능하지만 그 개념을 도입할 수 있는 모델(k-최근접 이웃, 신경망, 의사결정나무, 앙상블 모델 등)에도 역시 적용이 가능하다.

 

●관련 문법: predict_proba

확률 모델은 학습 목적식 자체를 바꾸는 것이 아니고 학습의 결과를 바꾸는 방식으로 predict_proba를 응용한다.

predict_proba는 sklearn의 확률 모델이 갖는 메서드(fit 이후)로서, X(새로 들어온 데이터, 평가 데이터 x_test)를 입력으로 받아 각 클래스에 속할 확률을 출력한다.

X(새로 들어온 데이터, 평가 데이터 x_test)에 포함된 모든 행에 대해 부정클래스와 긍정클래스에 속할 확률들을 출력해준다.

(참고: predict는 각 클래스에 속할 확률이 아닌 다이렉트로 예측값을 출력해준다, 예를 들어, 부정클래스 확률이 0.3 긍정 클래스 확률이 0.7이면 긍정클래스 확률이 더 크기 때문에 다이렉트로 긍정클래스(1)로 분류를 해주고 반대로 부정클래스 확률이 0.7이고 긍정 클래스 확률이 0.3이면 부정클래스 확률이 더 크기 때문에 다이렉트로 부정클래스(-1)로 분류를 해준다.)

 

X(새로 들어온 데이터, 평가 데이터 x_test)에 포함된 모든 행에 대해 부정클래스와 긍정클래스에 속할 확률들을 출력해준

뒤 긍정클래스에 속할 확률이 나오는 컬럼인 Pr(Pos l x)의 값이 c보다 크거나 같으면 긍정클래스로 볼 것이고 c보다 작으면 부정클래스로 볼 것이다.

이것을 구현 할때 필요한 개념이 있다. Numpy와 Pandas를 사용할 때 가능하면 배열 단위 연산을 하는 것이다.

다시말해, 유니버설 함수, 브로드캐스팅, 마스크 연산을 최대한 활용해야 한다.

(예시) 확률 모델의 cut_off_value를 0.3으로 설정하기

긍정클래스에 속할 확률이 나오는 컬럼인 Pr(Pos l x)의 값을 0.3보다 크거나 같은지를 본다. 여기서 긍정클래스에 속할 확률이 나오는 컬럼인 Pr(Pos l x)의 값들은 배열이고 0.3은 하나의 스칼라 숫자로 브로드캐스팅을 통해서 각각의 값들이 비교가 될 것이다. 그리고 0.3 이상인 것은 True를 그렇지 않은 것은 False로 나올 것이다. 

그 후 클래스가 1과 -1이다라고 하면 False를 -1로 True를 1로 바꿔야한다. replace를 사용해서 .replace{"False":-1, "True":1}로 바꿀수 있지만 2*R-1로 단순한 연산을 통해 바꿀 수도 있다.(False는 0, True는 1이기 때문)

(부울을 -1과 1로 바꾸는 테크닉 2*R-1 도 알아두면 도움이 된다.)

 

비확률 모델

비확률 모델은 확률 모델과 다르게 학습 목적식 자체를 바꾸는 것이다.

 

(1) 서포트 벡터 머신

일반 서포트 벡터 머신의 목적식에서 왼쪽에 있는 llwll가 마진을 최대화하는 부분, 오른쪽 C*크사이i가 오차를 최소화 하는 부분이다.

크사이i는 샘플i에 대한 오차이다. 다시 말해, 샘플i에 대한 오차가 위양성비용인지 위음성비용인지 구분이 안되있고 샘플i에 대한 그냥 오차이다. 

비용 민감 서포트 벡터 머신 목적식에서 비용을 두 개로 구분했다. 

yi가 1일 때 발생하는 비용
yi가 -1일 때 발생하는 비용

yi가 1일 때 발생하는 비용이라는 것은 yi가 1인데 예측을 -1로 했다는 소리이다. 즉, 긍정을 부정으로 오분류한 비용이다.

그러면 yi가 -1일 때 발생하는 비용은 부정을 긍정으로 오분류한 비용이다.

우리가 관심이 있는 것은 긍정을 부정으로 오분류한 비용으로 C1을 C2보다 크게 둬야한다.

 

(2)의사결정나무

소수 클래스에 대한 가중치를 부여하는 방식으로 가능하면 소수 클래스로 분류하도록 유도하고 k-최근접 이웃, 앙상블 모델(랜덤포레스트, xgboost, lightgbm)에도 똑같이 정의할 수 있다. 

동그라미가 다수클래스(우리의 관심 대상이 아닌 부정클래스)고 세모가 소수클래스(우리의 관심 대상 긍정클래스)라고 하면

w1을 1로 두고, w2는 긍정이 부정보다 중요한 정도를 나타내도록 설정한다. 보통은 w1을 1로 두고 w2를 클래스 불균형 비율, 클래스 불균형 비율보다 조금 작은 정도로 두는 경우가 많다.

 

●관련 문법: class_weight

의사결정나무, 서포트벡터머신, 랜덤포레스트, xgboost, lightgbm 등에서는 class_weight를 설정함으로써 비용 민감 모델을 구현할 수 있다. class_weight는 사전 형태로 입력한다.▷key: class 이름

▷value: class weight

(예시) SVC(class_weight = {1:10, -1:1}) → 클래스 1에 클래스 -1보다 10배의 가중치를 부여

 


## 코드 실습 ##

●확률 모델의 비용 민감 모델 구현

import os
os.chdir(r"C:\Users\82102\Desktop\데이터 전처리\머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("Secom.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Y', axis=1)
Y = df['Y']
# 학습 데이터와 평가 데이터 분할
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 클래스 불균형 확인
y_train.value_counts()

# 클래스 불균형 비율 계산
y_train.value_counts().iloc[0] / y_train.value_counts().iloc[-1]

# KNN을 사용한 클래스 불균형 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import *

KNN_model = KNN(n_neighbors = 11).fit(x_train, y_train)
pred_y = KNN_model.predict(x_test)
print("정밀도:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))
# 재현율이 0%로 불균형이 심각한 수준이라 판단

# 비용 민감 모델 적용전 logistic regression 모델 테스트
from sklearn.linear_model import LogisticRegression as LR

# max_iter는 Gradient Descent 방식을 반복해서 몇번 수행할 것인가 인데, 이게 일단 수렴(Convergence)하게 되면 횟수를 늘려도 성능이 거의 달라지지 않음
# max_iter는 경사하강법을 통해 weight값을 최적화 즉, 가장 최적화된 모델을 만들기 위해서 경사하강법을 몇 번 수행 할 것인지에 대한 설정
model = LR(max_iter = 100000).fit(x_train, y_train)
pred_y = model.predict(x_test)
print("정밀도:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

# cut off value 조정
probs = model.predict_proba(x_test)
probs = pd.DataFrame(probs, columns = model.classes_)

cut_off_value = 0.3

pred_y = 2 * (probs.iloc[:, -1] >= cut_off_value) -1
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

# cut off value를 조정하는 함수 작성
def cost_sensitive_model(model, cut_off_value, x_test, y_test):
    probs = model.predict_proba(x_test)
    probs = pd.DataFrame(probs, columns = model.classes_)
    pred_y = 2 * (probs.iloc[:, -1] >= cut_off_value) -1
    recall = recall_score(y_test, pred_y)
    accuracy = accuracy_score(y_test, pred_y)
    return recall, accuracy
# cut off value에 따른 recall과 accuracy 변화 확인
from matplotlib import pyplot as plt
import numpy as np

cut_off_value_list = np.linspace(0, 1, 101)
recall_list = []
accuracy_list = []

for c in cut_off_value_list:
    recall, accuracy = cost_sensitive_model(model, c, x_test, y_test)
    recall_list.append(recall)
    accuracy_list.append(accuracy)

%matplotlib inline
plt.plot(cut_off_value_list, recall_list, label = 'recall')
plt.plot(cut_off_value_list, accuracy_list, label = 'accuracy')
plt.legend()
# 그래프를 통해 대략적인 정확도와 재현율의 변화를 보고 cut off value를 찍어보면서 조정해본다

 

 

●비확률 모델의 비용 민감 모델 구현

import os
os.chdir(r"C:\Users\82102\Desktop\데이터 전처리\머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("page-blocks0.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Class', axis=1)
Y = df['Class']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 클래스 불균형 확인
y_train.value_counts()

# 라벨이 문자이니 변환
y_train.replace({"negative":-1, "positive":1}, inplace=True)
y_test.replace({"negative":-1, "positive":1}, inplace=True)
# 클래스 불균형 비율 계산
y_train.value_counts().iloc[0] / y_train.value_counts().iloc[-1]

# KNN을 사용한 클래스 불균형 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import *

KNN_model = KNN(n_neighbors = 11).fit(x_train, y_train)
pred_y = KNN_model.predict(x_test)
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))
# 재현율을 보니 불균형이 심각한 수준은 아니라고 판단

# 비용 민감 모델 적용전 support vector machine 모델 테스트(클래스 웨이트 조정 전)
from sklearn.svm import SVC

model = SVC().fit(x_train, y_train)
pred_y = model.predict(x_test)
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

# 클래스 웨이트 조정
model = SVC(class_weight = {1:8, -1:1}).fit(x_train, y_train)
pred_y = model.predict(x_test)
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

 

cut off value랑 class weight 값도 튜닝하면 좋을 것 같긴한데...

재샘플링은 클래스 비율이 맞지 않아서(클래스 변수가 불균형 한 것을) 처리하기 위한 방법이다. 

→클래스 불균형 발생 원인 첫번째 case를 처리하기 위한 방법

 

오버샘플링과 언더샘플링

언더샘플링은 결정 경계에 가까운 다수 클래스 샘플을 제거하고, 오버샘플링은 결정 경계에 가까운 소수 클래스 샘플을 생성해야 한다. 왜냐하면 기본적으로 클래스 불균형 문제 해소는 소수 클래스에 대한 결정 공간을 넓히는 것이기 때문이다. 

아래 그림에서 현재 결정 경계가 주황색 선으로 표시되어있다면 화살표 방향대로 움직여야 결정 공간이 넓어질 것이다. 

소수 클래스 샘플을 더 만들어야 하는 오버샘플링일때 결정 경계에 가까운 소수 클래스 샘플을 만들어야 결정 경계가 앞쪽으로 밀려서 결정 공간이 넓어질 것이다. 다수 클래스 샘플을 제거해야하는 언더샘플링일 때 결정 경계에 가까운 다수 클래스 샘플을 제거해야 결정 경계가 앞쪽으로 밀려서 결정 공간이 넓어질 것이다.(군사 경계선을 생각하면 이해하기가 쉽다.)

재샘플링할때 주의 사항으로는 평가 데이터에 대해서는 절대로 재샘플링을 적용하면 안된다.

오버샘플링을 평가 데이터에 썼을 때 가짜 데이터를 포함한 상태에서 평가를 하는 것이니 당연히 객관적인 평가 결과가 아니다. 그리고 가짜로 만든 데이터는 우리가 가지고 있는 데이터와 흡사하게 만들어 질수 밖에 없어서 학습 데이터와 평가 데이터가 굉장히 유사해진다는 소리이다. 그래서 평가 결과가 좋아지겠지만 그 결과는 신뢰할만한 결과가 아니다.

언더샘플링을 평가 데이터에 썼을 때 평가 데이터에서 제거되는 것은 보통 평가하기 어려운 샘플들이 제거된다. 그렇기 때문에 평가 결과가 좋아지겠지만 그 결과는 신뢰할만한 결과가 아니다. (ex.평가를 100개 했어야 했는데 언더샘플링을 함에 따라 50개만 하게된다. 그리고 제거되는 것은 보통 평가하기 어려운 샘플들이 제거된다.)

 

대표적인 오버샘플링 알고리즘: SMOTE

SMOTE(Synthetic Minority Over-Sampling Technique)는 2002년에 제안된 기법으로, 대부분의 오버 샘플링 기법이 이 기법에 기반하고 있다.

SMOTE 원리

소수 클래스 샘플을 임의로 선택하고, 선택된 샘플의 소수 클래스 이웃 가운데 하나의 샘플을 또 임의로 선택하여 그 중간에 샘플을 생성하는 과정을 반복하는 방법이다.

default는 소수 클래스 샘플을 다수 클래스 샘플과 동일하게 1대1로 만드는데, 이럴경우 재현율은 높아지지만 정확도가 너무 떨어지는 상황이 발생할 수 있다. 즉, 클래스 불균형 문제를 완전 해소하는 것은 오히려 역효과를 일으킬수 있기 때문에 적당한 수치 내에서 조정하는 것이 필요하다. 정답은 없지만 보통 3대1, 4대1 정도로 만든다.

 

●관련 문법: imblearn.over_sampling.SMOTE

-주요 입력

▶sampling_strategy: 입력하지 않으면 1대1 비율이 맞을 때까지 샘플을 생성하며, 사전 형태로 입력하여 클래스별로 샘플 개수를 조절 가능

▶k_neighbors: SMOTE에서 고려하는 이웃 수(보통 1, 3, 5 정도로 작게 설정)

 

-주요 메서드

▶.fit_resample(X, Y): X와 Y에 대해 SMOTE를 적용한 결과를 ndarray 형태로 반환

 

→sklearn의 scaler, preprocessing 인스턴스랑 굉장히 유사하지만 fit이 없고 fit과 sample이 따로 있지 않고 붙어서 쓴다.(sample은 transform이랑 비슷하다고 생각)그 이유는 재샘플링은 평가 데이터에 대해서는 절대로 적용하면 안되기 때문이다. 그러면 평가 데이터가 없는데 굳이 fit 따로 sample 따로 할 필요가 없어서 fit_sample 한번 하고 끝낸다. 그래서 fit 따로 sample 따로 하는 구문이 없고 fit_sample로 한번에 코드를 작성한다.

 

 

대표적인 언더샘플링 알고리즘: NearMiss

가장 가까운 n개의 소수 클래스 샘플까지 평균 거리가 짧은 다수 클래스 샘플을 순서대로 제거하는 방법이다.

NearMiss 원리

평균 거리가 짧은 다수 클래스 샘플을 왜 제거하는지 알아보면 소수 클래스 샘플과 가깝다는 것은 결정 경계 주위에 있다고 볼 수 있다. 그렇기 때문에 결정 경계에 가까운 것을 지우기 위해서 소수 클래스 샘플까지 거리를 활용한다고 볼 수 있다.

n은 소수 클래스 샘플 개수만큼 설정하는 것이 일반적이다.

 

●관련 문법: imblearn.under_sampling.NearMiss

-주요 입력

▶sampling_strategy: 입력하지 않으면 1대1 비율이 맞을 때까지 샘플을 제거하며, 사전 형태로 입력하여 클래스별로 샘플 개수를 조절 가능

▶n_neighbors: 평균 거리를 구하는 소수 클래스 샘플 수

▶version: NearMiss의 version으로, 2를 설정하면 모든 소수 클래스 샘플까지의 평균 거리를 사용

→버전이 1, 2, 3이 있고 그 중 제일 많이 사용하는 버전이 2이다. 버전 2는 n_neighbors를 모든 소수 클래스 샘플 수랑 같게 설정하는 것이다. 버전 1(위의 예시는 버전 1)을 썼을 때 평균 거리를 구하는 소수 클래스 샘플 수(n_neighbors)를 설정해야 하기 때문에 즉, 하이퍼 파라미터가 늘어나기 때문에 하이퍼 파라미터를 설정하는데 근거도 없고 하나하나 비교했을 때 성능 차이가 크지 않기 때문에 보통 버전 2를 사용한다.

 

-주요 메서드

▶.fit_resample(X, Y): X와 Y에 대해 NearMiss를 적용한 결과를 ndarray 형태로 반환

 


## 코드 실습 ##

●오버샘플링-SMOTE 사용

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("Secom.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Y', axis=1)
Y = df['Y']
# 학습 데이터와 평가 데이터 분할
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 클래스 불균형 확인
# 언더샘플링을 적용하기에는 클래스 1에 대한 값이 너무 적어서 남는 샘플이 너무 부족해지기 때문에 부적절하다고 판단
y_train.value_counts()

# 클래스 불균형 비율 계산
y_train.value_counts().iloc[0] / y_train.value_counts().iloc[-1]

# KNN을 사용한 클래스 불균형 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import *

knn_model = KNN(n_neighbors = 11).fit(x_train, y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율이 0%로 불균형이 심각한 수준

# 오버샘플링:SMOTE
from imblearn.over_sampling import SMOTE

# SMOTE 인스턴스 생성
oversampling_instance = SMOTE(k_neighbors = 3) # sampling_strategy를 설정 안했기 때문에 default로 1대1 비율로 맞춰짐

# 오버샘플링 적용
o_x_train, o_y_train = oversampling_instance.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
o_x_train = pd.DataFrame(o_x_train, columns = X.columns)
o_y_train = pd.Series(o_y_train)
o_y_train.value_counts() # 클래스 비율이 1대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 1대1)
knn_model = KNN(n_neighbors = 11).fit(o_x_train, o_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 정확도는 감소했으나, 재현율이 크게 오름을 확인
# 결론: 재현율이 크게 오른 것은 좋지만 정확도가 너무 떨어졌기 때문에 정확도를 어느정도 보장을 한 상태에서 재현율을 올리고 싶다고 판단

# 클래스 비율이 1대1이 아닌 2대1로 설정
# SMOTE 인스턴스 생성
oversampling_instance = SMOTE(k_neighbors = 3, sampling_strategy = {1:int(y_train.value_counts().iloc[0] / 2),
                                                                   -1:y_train.value_counts().iloc[0]})

# 오버샘플링 적용
o_x_train, o_y_train = oversampling_instance.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
o_x_train = pd.DataFrame(o_x_train, columns = X.columns)
o_y_train = pd.Series(o_y_train)
o_y_train.value_counts() # 클래스 비율이 2대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 2대1)
knn_model = KNN(n_neighbors = 11).fit(o_x_train, o_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율은 아까보다 좋아지지 않았지만 정확도는 조금 보장됨

 

 

●언더샘플링-NearMiss 사용

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("page-blocks0.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Class', axis=1)
Y = df['Class']
# 학습 데이터와 평가 데이터 분할
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 클래스 불균형 확인
y_train.value_counts()
# positve 즉, 클래스 1에 대한 값을 negative 즉, 클래스 -1에 맞춰 오버샘플링을 진행하면 데이터가 너무 많아지고 계산량이 많아지기 때문에
# 언더샘플링이 적합하다고 판단

y_train.replace({"negative":-1, "positive":1}, inplace=True)
y_test.replace({"negative":-1, "positive":1}, inplace=True)
# 클래스 불균형 비율 계산
y_train.value_counts().iloc[0] / y_train.value_counts().iloc[-1]
# 9를 넘지 못하지만 약간 심각한 수치라고 봄

# KNN을 사용한 클래스 불균형 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import *

knn_model = KNN(n_neighbors = 11).fit(x_train, y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율이 0%가 아니고 50%로 불균형이 심각한 수준은 아니라고 보임
# 일단 언더샘플링을 진행해본다

# 언더샘플링:NearMiss
from imblearn.under_sampling import NearMiss

# NearMiss 인스턴스 생성
NM_model = NearMiss(version = 2) # version = 2: 모든 소수 클래스 샘플까지의 평균 거리를 사용

# 언더샘플링 적용
u_x_train, u_y_train = NM_model.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
u_x_train = pd.DataFrame(u_x_train, columns = X.columns)
u_y_train = pd.Series(u_y_train)
u_y_train.value_counts() # 클래스 비율이 1대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 1대1)
knn_model = KNN(n_neighbors = 11).fit(u_x_train, u_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율은 크게 올랐으나, 정확도가 크게 떨어짐 -> 적당한 비율에 맞게 설정해야 함

# 클래스 비율이 1대1이 아닌 5대1로 설정
# NearMiss 인스턴스 생성
NM_model = NearMiss(version = 2, sampling_strategy = {1:y_train.value_counts().iloc[-1],
                                                     -1:y_train.value_counts().iloc[-1] * 5})

# 언더샘플링 적용
u_x_train, u_y_train = NM_model.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
u_x_train = pd.DataFrame(u_x_train, columns = X.columns)
u_y_train = pd.Series(u_y_train)
u_y_train.value_counts() # 클래스 비율이 5대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 5대1)
knn_model = KNN(n_neighbors = 11).fit(u_x_train, u_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율도 올랐고 정확도도 약간 보장됨


정확도와 재현율의 변화 추이를 보고 적절한 클래스 비율을 선택해야한다.

+ Recent posts