파이썬 머신러닝 완벽 가이드 - 5. Classification(3) (예제 및 스태킹)

Mios·2022년 10월 4일
1
post-thumbnail

4. 실전 예시

A. Santander 예시

  1. EDA 중, head, info, describe 사용
  2. 이상치 발생 ⇒ 제일 많은 걸로 대체하는 방법론도 있음

B. 신용카드 사기 검출 예시

  • LGBMClassifier( . . . boost_from_average=False) : 레이블값 매우 불균형한 경우 False, if True ⇒ 재현률 및 ROC-AUC 성능 매우 저하
    ( : 왜인지는 아직 모름)
  1. 언더 샘플링과 오버 샘플링의 이해 : 지도 학습시 극도로 불균형한 레이블 값 분포로 인한 문제점을, 적절한 학습 데이터를 확보로 해결 방법들
    (주로 오버 샘플링 방식이 예측 성능상 더 유리한 경우가 많아 주로 사용됨)

- 언더 샘플링 : 많은 데이터 세트를 적은 데이터 세트 수준으로 감소시키는 방식 ⇒ 너무 많은 정상 레이블 데이터를 감소시켜서, 오히려 학습이 잘 안될 수 있음
- 오버 샘플링 : 적은 데이터 세트를 많은 데이터 세트 수준으로 증식시키는 방식 ⇒ 동일한 데이터를 단순 증식하는 건 과적합 되기에 의미 X ⇒ 원본 피처 값들을 아주 약간만 변경하여 증식함
    1. 대표적으로 SMOTE(Synthetic Minority Over-sampling Technique) 방법이 있음
    2. SMOTE는 적은 제이터 세트에 있는 개별 데이터들의 K 최근접 이웃을 찾아서 이 데이터와 K개 이웃들의 차이를 일정 값으로 만들어거 기존 데이터와 약간 차이가 나는 새로운 데이터들을 생성하는 방식
    3. SMOTE를 구현하는 파이썬 패키지 == imbalanced-learn
        

  1. (전처리) 데이터 분포도 변환

    1. StandardScaler : 로지스틱 회귀 같은 선형 회귀 경우, 중요 피처값들이 정규분포 유지하는 것을 선호함.

      1. 중요한 피처인 Amount를 sns.distplot 해보니 긴 꼬리 형태 ⇒ 정규분포형태로 전처리 (StandarScaler)
      2. 별로 큰 효과는 없었음
      from sklearn.preprocessing import StandardScaler
      # 사이킷런의 StandardScaler를 이용하여 정규분포 형태로 Amount 피처값 변환하는 로직으로 수정. 
      def get_preprocessed_df(df=None):
          df_copy = df.copy()
          scaler = StandardScaler()
          print(df_copy['Amount'].values)
          amount_n = scaler.fit_transform(df_copy['Amount'].values.reshape(-1, 1))
          # 변환된 Amount를 Amount_Scaled로 피처명 변경후 DataFrame맨 앞 컬럼으로 입력
          df_copy.insert(0, 'Amount_Scaled', amount_n)
          # 기존 Time, Amount 피처 삭제
          df_copy.drop(['Time','Amount'], axis=1, inplace=True)
          return df_copy
    2. 로그 변환 : 데이터 분포도가 심하게 왜곡돼있을 때 사용하면 좋음

      1. 원래 값을 log 값으로 변환해, 원래 큰 값을 상대적으로 작은 값으로 변환해주기에 데이터 분포도의 왜곡을 상당 수준 개선해 줌
      2. 약간식 개선됨
      def get_preprocessed_df(df=None):
          df_copy = df.copy()
          # 넘파이의 log1p( )를 이용하여 Amount를 로그 변환 
          amount_n = np.log1p(df_copy['Amount'])
          df_copy.insert(0, 'Amount_Scaled', amount_n)
          df_copy.drop(['Time','Amount'], axis=1, inplace=True)
          return df_copy
  2. (전처리) 이상치 데이터 제거

    1. IQR (Inter Quantile Range) : 사분위 값의 편차를 이용하는 기법 → Q1(25%) ~ Q3(75%) 범위를 IQR이라고 부름 (중앙값에서 퍼져나간 정도)
      ⇒ IQR = Q3 - Q1
      ⇒ Box Plot으로 시각화
    2. IQR 이용해 이상치 데이터 검출하는 방식 : IQR 1.5 하여 생성된 범위를 이용해, 최댓/최솟값을 결정한 뒤, 여기를 벗어나는 데이터를 이상치로 간주 ⇒ 경우에 따라 1.5가 변경될 수 있음
      ⇒ Q3(3/4분위수)에, IQR
      1.5를 더함 == 최댓값
      ⇒ Q2(1/2분위수) == 중앙값
      ⇒ Q1(1/4분위수)에, IQR*1.5를 뻄 == 최솟값

    1. 매우 많은 피처가 있을 경우, 이들 중 결정값(레이블)과 가장 상관성이 높은 피처 위주로 이상치를 검출해야 시간/성능에 유리함

      import seaborn as sns
      
      plt.figure(figsize=(9, 9))
      corr = card_df.corr()
      sns.heatmap(corr, cmap='RdBu')
    2. IQR을 이용해, 이상치를 검출하는 함수 생성 ⇒ IQR 계산 ⇒ 최댓/최솟값 아웃라이어 찾기

      import numpy as np
      
      def get_outlier(df=None, column=None, weight=1.5):
          # fraud에 해당하는 column 데이터만 추출, 1/4 분위와 3/4 분위 지점을 np.percentile로 구함. 
          fraud = df[df['Class']==1][column] # column = 'V14'
          quantile_25 = np.percentile(fraud.values, 25)
          quantile_75 = np.percentile(fraud.values, 75)
          # IQR을 구하고, IQR에 1.5를 곱하여 최대값과 최소값 지점 구함. 
          iqr = quantile_75 - quantile_25
          iqr_weight = iqr * weight
          lowest_val = quantile_25 - iqr_weight
          highest_val = quantile_75 + iqr_weight
          # 최대값 보다 크거나, 최소값 보다 작은 값을 아웃라이어로 설정하고 DataFrame index 반환. 
          outlier_index = fraud[(fraud < lowest_val) | (fraud > highest_val)].index
          return outlier_index
      
      outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
      print('이상치 데이터 인덱스:', outlier_index)
  3. (전처리) SMOTE 오버 샘플링 적용

    1. SMOTE 적용시에는 반드시 train(학습) 데이터 세트만 오버 샘플링 해야함 → eval(검증), test(테스트) 데이터 세트 하면 올바른 검증/테스트 불가

      from imblearn.over_sampling import SMOTE
      
      smote = SMOTE(random_state=0)
      X_train_over, y_train_over = smote.fit_resample(X_train, y_train) #fit_sample 없어지고 -> fit_resample로 바뀜
      print('SMOTE 적용 전 학습용 피처/레이블 데이터 세트: ', X_train.shape, y_train.shape)
      print('SMOTE 적용 후 학습용 피처/레이블 데이터 세트: ', X_train_over.shape, y_train_over.shape)
      print('SMOTE 적용 후 레이블 값 분포: \n', pd.Series(y_train_over).value_counts())
      
      lr_clf = LogisticRegression()
      # ftr_train과 tgt_train 인자값이 SMOTE 증식된 X_train_over와 y_train_over로 변경됨에 유의
      get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)
      1. 재현율/AUC는 엄청 올라가지만, 정밀도, 정밀도/F1은 급격히 내려감 ⇒ 실무에 적용할 수 없음
      2. 실제 원본 데이터의 유형보다 너무나 많은 Class=1 데이터가 학습되어, 테스트 데이터 세트 예측에서 Class=1 예측이 너무 많아짐
    2. precision_recall_curve_plot() 으로 확인해보자 (추후 threshold를 변경하려나…?)

      import matplotlib.pyplot as plt
      import matplotlib.ticker as ticker
      from sklearn.metrics import precision_recall_curve
      %matplotlib inline
      
      def precision_recall_curve_plot(y_test , pred_proba_c1):
          # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출. 
          precisions, recalls, thresholds = precision_recall_curve( y_test, pred_proba_c1)
          
          # X축을 threshold값으로, Y축은 정밀도, 재현율 값으로 각각 Plot 수행. 정밀도는 점선으로 표시
          plt.figure(figsize=(8,6))
          threshold_boundary = thresholds.shape[0]
          plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
          plt.plot(thresholds, recalls[0:threshold_boundary],label='recall')
          
          # threshold 값 X 축의 Scale을 0.1 단위로 변경
          start, end = plt.xlim()
          plt.xticks(np.round(np.arange(start, end, 0.1),2))
          
          # x축, y축 label과 legend, 그리고 grid 설정
          plt.xlabel('Threshold value'); plt.ylabel('Precision and Recall value')
          plt.legend(); plt.grid()
          plt.show()
      
      precision_recall_curve_plot( y_test, lr_clf.predict_proba(X_test)[:, 1] )
      1. 실제로 그려보니, 임계값이 0.99이하에서는 재현율이 매우 좋고/정밀도가 극단적으로 낮다가, 0.99에는 급격히 서로 cross 됨
      2. 이건, threshold 바꿔도 성능을 얻을 수 없으므로, 이번 사례에서 로지스틱 회귀의 경우에는 SMOTE 적용하면 안된다는 결론
    3. LightGBM의 경우에는 SMOTE 활용하면, 이상치만 제거한 것에 비해 재현율은 높아지나, 정밀도는 낮아짐

      1. 일반적으로 SMOTE를 쓰면 재현율은 높아지나, 정밀도는 낮아짐

5. 스태킹 Stacking

  1. 스태킹 앙상블 : 개별 알고리즘의 예측 결과 데이터 세트를 “최종적인 메타 데이터 세트”로 만들어 별도의 ML 알고리즘으로 최종학습을 수행하고 테스트 데이터를 기반으로 다시 최종 예측을 수행하는 방식

    • 메타 모델: 개별 모델의 예측된 데이터 세트를 다시 기반으로 하여 학습하고 예측하는 방식

    • 필요한 모델

      1. 개별적인 기반 모델

      2. 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 “최종 메타 모델”

        # 개별 모델들을 학습/예측
        
        knn_pred = knn_clf.predict(X_test)
        rf_pred = rf_clf.predict(X_test)
        dt_pred = dt_clf.predict(X_test)
        ada_pred = ada_clf.predict(X_test)
        
        pred = np.array([knn_pred, rf_pred, dt_pred, ada_pred])
        print(pred.shape)
        
        # transpose를 이용해 행과 열의 위치 교환. 컬럼 레벨로 각 알고리즘의 예측 결과를 피처로 만듦. 
        pred = np.transpose(pred)
        print(pred.shape)
        
        lr_final.fit(pred, y_test)
        final = lr_final.predict(pred)
        
        print('최종 메타 모델의 예측 정확도: {0:.4f}'.format(accuracy_score(y_test , final)))
  2. CV 세트 기반의 스태킹 : 과적합 개선 위해, 최종 메타 모델을 위한 데이터 세트를 만들 때, 교차 검증 기반으로 예측된 결과 데이터 세트를 이용한다.

    1. 위 스태킹 코드에서, 로지스틱 회귀 메타모델 최종 학습시, 학습 데이터가 아닌 테스트용 데이터 기반으로 학습했기에 과적합이 발생할 수 있음
      1. 각 모델을 train data로 학습시킨 후
      2. 개별모델_pred = 개별모델.predict(X_test) ⇒ X_test 즉, 테스트용 데이터로 학습 시키고
      3. 최종 메타 모델때 y_test로 예측했으니 과적합 가능성 존재
    2. CV 세트 기반의 스태킹은 개별 모델들이, 각각 교차 검증으로,
      [메타 모델을 위한 “학습용 스태킹 데이터 생성”] / [예측을 위한 “테스트용 스태킹 데이터”를 생성]한 뒤,
      이를 기반으로 모델이 학습과 예측을 수행
      ⇒ 2단계의 스텝으로 구분

  3. CV 기반의 스태킹 모델 Step

    1. 각 모델 별로 원본 학습/테스트 데이터를 예측한 결과 값을 기반으로 메타 모델을 위한 학습/테스트용 데이터 생성
      1. 원본 학습 데이터 n(폴드)-1개로 학습된 개별 모델을 만듦
      2. 개별 모델로 원본 학습 데이터 폴드 1개를 Val 예측으로 [메타 학습 데이터] 칸 순차적으로 채우기
      3. 개별 모델로 원본 테스트 데이터 예측해서 결괏값 순차적으로 나열(?)하기
      4. 위 3개의 작업을 cv=n, n번 반복하면 ⇒ [메타 학습 데이터] 완성
      5. 그리고 n개의 원본 테스트 데이터에 대한 결괏값에 대한 AVG 구해서 [메타 테스트 데이터] 만들기
    2. [메타 학습/테스트 데이터] 완성 됐으니 그 다음부턴 그냥 fit, predict하면 됨
    # Step 1
    
    from sklearn.model_selection import KFold
    from sklearn.metrics import mean_absolute_error
    
    # 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터를 생성하기 위한 함수. 
    def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds ):
        # 지정된 n_folds값으로 KFold 생성.
        kf = KFold(n_splits=n_folds, shuffle=False) #, random_state=0)
        #추후에 메타 모델이 사용할 학습 데이터 반환을 위한 넘파이 배열 초기화 
        train_fold_pred = np.zeros((X_train_n.shape[0] ,1 ))
        test_pred = np.zeros((X_test_n.shape[0],n_folds))
        print(model.__class__.__name__ , ' model 시작 ')
        
        for folder_counter , (train_index, valid_index) in enumerate(kf.split(X_train_n)):
            #입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 셋 추출 
            print('\t 폴드 세트: ',folder_counter,' 시작 ')
            X_tr = X_train_n[train_index] 
            y_tr = y_train_n[train_index] 
            X_te = X_train_n[valid_index]  
            
            #폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수행.
            model.fit(X_tr , y_tr)       
            #폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델 예측 후 데이터 저장.
            train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1,1)
            #입력된 원본 테스트 데이터를 폴드 세트내 학습된 기반 모델에서 예측 후 데이터 저장. 
            test_pred[:, folder_counter] = model.predict(X_test_n)
                
        # 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성 
        test_pred_mean = np.mean(test_pred, axis=1).reshape(-1,1)    
        
        #train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
        return train_fold_pred , test_pred_mean
    
    knn_train, knn_test = get_stacking_base_datasets(knn_clf, X_train, y_train, X_test, 7)
    rf_train, rf_test = get_stacking_base_datasets(rf_clf, X_train, y_train, X_test, 7)
    dt_train, dt_test = get_stacking_base_datasets(dt_clf, X_train, y_train, X_test,  7)    
    ada_train, ada_test = get_stacking_base_datasets(ada_clf, X_train, y_train, X_test, 7)
    
    # 각 모델별 학습/테스트 데이터 합치기
    Stack_final_X_train = np.concatenate((knn_train, rf_train, dt_train, ada_train), axis=1)
    Stack_final_X_test = np.concatenate((knn_test, rf_test, dt_test, ada_test), axis=1)
    print('원본 학습 피처 데이터 Shape:',X_train.shape, '원본 테스트 피처 Shape:',X_test.shape)
    print('스태킹 학습 피처 데이터 Shape:', Stack_final_X_train.shape,
          '스태킹 테스트 피처 데이터 Shape:',Stack_final_X_test.shape)
    
    # 최종 메타 모델 돌리기
    lr_final.fit(Stack_final_X_train, y_train) # 최종 메타 모델 돌릴 때, 원본 학습 라벨 y_train 가져야 써야함
    stack_final = lr_final.predict(Stack_final_X_test)
    
    print('최종 메타 모델의 예측 정확도: {0:.4f}'.format(accuracy_score(y_test, stack_final)))

6. 정리

  • 분류 Classification
    • 결정 트리 Decision Tree
    • Voting
    • Bagging
      • RandomForestClassifier
    • Boosting
      • GBM
      • XGBoost
      • LightGBM
    • Stacking
profile
mios의 데이터 놀이터 | Instagram@data.decision (하단 홈 아이콘 버튼)

0개의 댓글