Study/머신러닝

머신러닝 Advanced_캐글 대회

김 도경 2024. 11. 9. 16:18

[2024.11.08] 필수 온라인 강의 Part16 Machine Learning Advanced CH08 실전 프로젝트

캐글 대회에서 사용하는 팁 (이론)

 

실험하기 전 고려할 사항들

  • 디버깅
    - 프로그래밍에서 오류를 찾아내고 수정하는 과정
    - 머신러닝 프로세스의 경우 한번 실행시 오래 걸리는 경우가 많아서 돌린 후 에러를 확인해서 수정하면 시간 소요가 큼

    디버깅모드
      - 실험 환경이 잘 설정되었는지 체크하기 위한 과정
      - 다양한 방식으로 프로세스를 축소하여 체크
         - 방법 1. 샘플 데이터
         - 방법 2. 작은 모델
         - 방법 3. 하이퍼파라미터 축소

  • 시드 고정
    - 시드 : 난수 생성기의 초기값으로 사용되는 값
      - 시드가 고정이 안된 경우 : 같은 코드에서 다른 점수가 발생 (재현에 실패)
      - 논문 및 모든 실험에서는 공정한 비교를 위해 재현이 되어야함 
      - 고정 방법 : 필요한 모든 부분에 seed (=42)를 고정

  • 실험 기록
    - Notion, Google Spreadsheet 등 다양한 툴을 활용하여 성능을 기록
         - 남들이 보여주기 위한 : 팀원들과 동일하게 진행X, 서로 인사이트를 얻기
         - 포트폴리오 만들기

    -  하나의 요소 실험 vs 여러 요소 동시에 실험
        - 일반적으로 조건을 하나씩 변경해가면서 실험하는 것을 추천
        - 성능과 자원의 트레이드 오프 관계가 존재하지만, 어떤 조건이 성능에 영향을 주었는지 파악하기 용이

 

앙상블(Ensemble) 기법
- 여러 모델을 결합하는 방법

  • 예시 KFold Ensemble
       - 모델의 조건(하이퍼파라미터)은 동일하지만 학습 데이터를 다르게 해서 학습한 K개의 모델들의 결과를 평균
         - 같은 모델링을 쓰지만, 데이터를 다르게 함

  • Model측면의 Ensemble -> 거의 필수적, 성능을 높이는데 가장 중요함
    - 모델을 다르게 해서 결합
      - 1. 서로 달라야한다(적용후에는 상관관계로 서로 다른 것을 사용하는 게 좋음
      - 2. 기본 베이스 성능 보장
      - 3. w1값을 검증을 통해서 찾기도 함(hyperparameter)
      - 모델 측면이 떨어지면, 서로 장단점을 합치는 게 좋음.
      - 서로가 잘하는 영역을 뽑아내서 사용하는 게 좋음 ->
    - e.g. LightGBM + Catboost / Random Forest = Decision Tree + Feature Selection / Linear Regression+Boosting 등등

  • Stacking
        - 모델의 예측 결과를 피처로 활용하는 방법
        - 대회에서의 솔루션으로는 정말 큰 역할을 함
        - 각 모델 별로 khold를 구해서, 각각의 결과에 얼마나 나왔는지 new feature로 활용.
        - 배포에는 어려움이 있지만, 성능을 높이는 방법이긴 하다
      
  • Seed Ensemble
      - 시드만 동일하게 해서 앙상블
      - 시드를 의도적으로 바꾸기도 함
       - 랜덤화 노후에 오는 위험성을 피하는데도 많이 사용을 함.
      - Private 모르는 경우 (미래 데이터 안정성)

추가 데이터

  • Pseudo Labeling
    - 데이터 셋에 가짜 (Pseudo) 레이블을 부여하는 방법
    - 캐글 및 대회에서는 평가 데이터셋에 레이블을 부여해서 학습에 활용하는 기법

    학습과정
       - 1. 일반적인 모델의 학습 진행
       - 2. 위의 모델로 평가 데이터셋에 대해 예측
       - 3. 예측한 평가 데이터셋과 학습 데이터셋을 결합 ***
       - 4. 3의 결과를 기반으로 새로운 모델 학습

  • 외부 데이터
    - Pseudo Labeling을 주로 활용
    - https://www.kaggle.com/competitions/hubmap-kidney-segmentation/discussion/238198

분석 도구

  • EDA 자동화 도구
    - Dataprep.eda : 데이터의 분석을 자동화 해주는 도구

  • 시각화 도구
    - Weights & Biases (WanDB) : 기록 & 분석 시각화 도구
    - Adversarial Validation : 기록 & 분석 시각화 도구
캐글 대회에서 사용하는 팁 (실전)


- Yahoo Finance OHLCV 데이터셋 사용

디버깅 및 재현성 확보 과정

# .sample을 통해서 데이터의 1% 만큼만 샘플링을 합니다.
if DEBUG_MODE:
    display(f"샘플링 전 데이터의 비율 : {X_train.shape}")
    X_train = X_train.sample(frac=0.01) # 원래는 _X_train 대신 X_train 으로 선언해야 해당 쉘 이후 코드가 문제 없이 돌아갑니다. 다만, 실습 자료에서는 이후 원할한 실습을 위해서 샘플링 전 원본 데이터로 진행하겠습니다.
    display(f"샘플링 후 데이터의 비율 : {_X_train.shape}")

 

- 재현성 확보

# 확보 전 : 돌릴때마다 달라지는 것을 확인할 수 있음
for i in range(5):
    display(f"시도 {i}번째")
    display(np.random.choice([1,2,3,4,5]))
    display(np.random.choice([1,2,3,4,5]))
    display(np.random.choice([1,2,3,4,5]))
    
# 확보 후 : [4,5,3] 계속 반복되는 것을 확인할 수 있음
for i in range(5):
    display(f"시도 {i}번째")
    np.random.seed(42)
    display(np.random.choice([1,2,3,4,5]))
    display(np.random.choice([1,2,3,4,5]))
    display(np.random.choice([1,2,3,4,5]))
# 일반적으로 대표적으로 고정하는 부분은 아래와 같습니다.
seed = 42
random.seed(seed)
np.random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)

# 만약 파이토치를 사용하는 경우
# torch.manual_seed(seed)
# torch.cuda.manual_seed(seed)
# torch.backends.cudnn.deterministic = True
# torch.backends.cudnn.benchmark = True

 

다양한 모델 앙상블 과정

1. 데이터 측면

- Kfold를 이용

 

- 앙상블 하기 전

# 베이스라인 용 데이터 셋 생성
holdout_X_train = X_train.copy()
holdout_Y_train = Y_train.copy()

# 날짜를 기준으로 최근 한 달의 데이터를 검증 데이터로 설정
holdout_X_train['Date'] = pd.to_datetime(holdout_X_train['Date'])
validation_start_date = holdout_X_train['Date'].max() - pd.Timedelta(days=30)
validation_indices = holdout_X_train[holdout_X_train['Date'] >= validation_start_date].index

# 위에서 얻은 인덱스를 사용하여 holdout_X_train 및 holdout_Y_train을 훈련 및 검증 데이터로 분리
holdout_X_valid = holdout_X_train.loc[validation_indices]
# 검증기간의 인덱스는 제외
holdout_X_train.drop(validation_indices, inplace=True)

holdout_Y_valid = holdout_Y_train[validation_indices]
# 검증기간의 인덱스는 제외
holdout_Y_train.drop(validation_indices, inplace=True)

display(f"수정된 학습 데이터의 기간: {holdout_X_train['Date'].min()} {holdout_X_train['Date'].max()}")
display(f"수정된 검증 데이터의 기간: {holdout_X_valid['Date'].min()} {holdout_X_valid['Date'].max()}")

# Gradient Boosting Machine을 선언
# 학습은 총 1,000번을 반복
gbm = lgb.LGBMRegressor(n_estimators=1000, random_state=42, subsample=0.7, subsample_freq=1)

# 학습을 진행
# 비교를 위해 %%time을 이용해 학습시간을 측정
# 다만, date변수의 경우 바로 학습에 사용할 수 없으니 제외
%%time
features = [c for c in holdout_X_train.columns if c not in ["Date"]]

gbm.fit(
    holdout_X_train[features],
    holdout_Y_train, # 학습 데이터를 입력
    eval_set=[
         (holdout_X_train[features], holdout_Y_train),
         (holdout_X_valid[features], holdout_Y_valid)
        ], # 평가셋을 지정합니다.
    eval_metric ='rmse', # 평가과정에서 사용할 평가함수를 지정
    callbacks=[
        lgb.early_stopping(stopping_rounds=10), # 10번의 성능향상이 없을 경우, 학습중지.
        lgb.log_evaluation(period=10, show_stdv=True)
        ] # 매 iteration마다 학습결과를 출력
)

# Test set에 대한 rmse를 측정
predicts = gbm.predict(X_test[features])
RMSE = mean_squared_error(Y_test, predicts)**0.5
display(f"Test rmse : {RMSE}")

- khold 앙상블 과정

# 학습 데이터를 StratifiedKFoldSplit로 나눕니다.
%%time
n_folds = 5
kf = StratifiedKFold(n_splits=5)

cut_Y_train = pd.cut(Y_train,
                     1000, # 데이터를 최소 최대 구간으로 1000등분 합니다.
                     labels=False)

train_folds = kf.split(X_train, cut_Y_train)
display(train_folds)

total_predicts = np.zeros(len(X_test))
total_oofs = np.zeros(len(X_train)) # array. [0, 0, ...]
for fold_idx, (train_idx, valid_idx) in enumerate(train_folds):
    display(f"--------{fold_idx}번째 fold의 학습을 시작합니다.--------")

    # index를 통해 fold의 학습세트를 가져옵니다.
    kfold_X_train = X_train.iloc[train_idx, :][features]
    kfold_Y_train = Y_train[train_idx]

    # index를 통해 fold의 평가세트를 가져옵니다.
    kfold_X_valid = X_train.iloc[valid_idx, :][features]
    kfold_Y_valid = Y_train[valid_idx]

    # fold의 데이터로 학습을 진행합니다.
    gbm = lgb.LGBMRegressor(n_estimators=1000, random_state=42, subsample=0.7)
    gbm.fit(
        kfold_X_train,
        kfold_Y_train, # 학습 데이터를 입력합니다.
        eval_set=[
             (kfold_X_train, kfold_Y_train),
             (kfold_X_valid, kfold_Y_valid)
             ], # 평가셋을 지정합니다.
        eval_metric ='rmse', # 평가과정에서 사용할 평가함수를 지정합니다.
        callbacks=[
            lgb.early_stopping(stopping_rounds=10), # 10번의 성능향상이 없을 경우, 학습을 멈춥니다.
            lgb.log_evaluation(period=10, show_stdv=True)
            ] # 매 iteration마다 학습결과를 출력합니다.
    )

    fold_predicts = gbm.predict(X_test[features])
    total_oofs[valid_idx] = gbm.predict(kfold_X_valid)
    # 각 fold의 rmse를 측정합니다.
    RMSE = mean_squared_error(Y_test, fold_predicts)**0.5
    display(f"Fold {fold_idx} - Test rmse : {RMSE}")

    total_predicts += fold_predicts / n_folds

RMSE = mean_squared_error(Y_train, total_oofs)**0.5
display(f"최종 Valid rmse : {RMSE}")

RMSE = mean_squared_error(Y_test, total_predicts)**0.5
display(f"최종 Test rmse : {RMSE}")

 

앙상블 전후에 따른 성능을 비교
Training Time : 13.7s ---> 104s       ( 속도는 오래 걸림)
Valid RMSE : 3658
Test RMSE : 3457.82 ---> 3327       (성능면에서는 개선)

 

- Filter methods

def get_highly_correlated_features(df, threshold=0.8):
    """
    높은 상관계수를 가진 변수 쌍 중에서 한 변수를 제외한 나머지 변수들의 이름을 반환합니다.

    Parameters:
    - df: 데이터프레임
    - threshold: 상관계수 임계값

    Returns:
    - 선택된 변수명 리스트
    """

    # 상관계수 행렬 계산
    # [[1, 0.5, 0.3], [0.5, 1, ..], [...]]
    correlation_matrix = df.corr()

    # 제거 대상 변수들을 저장할 집합
    to_remove = set()

    for i in range(correlation_matrix.shape[0]):
        for j in range(i + 1, correlation_matrix.shape[1]):
            if abs(correlation_matrix.iloc[i, j]) > threshold:
                to_remove.add(correlation_matrix.columns[j])

    # 제거 대상 변수를 제외하고 남은 변수들의 리스트 반환
    selected_features = [col for col in df.columns if col not in to_remove]
    return selected_features

features1 = get_highly_correlated_features(X_train[features], threshold=0.7)

display("Filter Method : ", features1)

 

- Feature Importance

def get_important_features(model, X_train, Y_train, threshold=0.8, upper=True):
    """
    Feature Importance를 사용하여 변수 중요도 상위 80%의 변수명을 반환합니다.

    Parameters:
    - model: 중요도를 추출한 모델
    - X_train: 학습 데이터의 독립변수
    - Y_train: 학습 데이터의 종속변수
    - threshold: 중요도 비율 (0.8은 상위 80%를 의미)

    Returns:
    - 선택된 변수명 리스트
    """

    # 변수 중요도와 변수명을 함께 저장
    feature_importances = list(zip(X_train.columns, model.feature_importances_))

    # 변수 중요도를 기준으로 정렬
    sorted_features = sorted(feature_importances, key=lambda x: x[1], reverse=True if upper else False)

    # 상위 threshold(예: 80%) 비율에 해당하는 변수의 수
    num_features_to_keep = int(threshold * len(sorted_features))

    # 상위 threshold(예: 80%)의 변수명 선택
    selected_features = [feature[0] for feature in sorted_features[:num_features_to_keep]]

    return selected_features

%%time
# Forward Feature Selection
forest_rf = RandomForestRegressor(
    n_estimators=50,
    criterion='squared_error',
    random_state=seed,
    n_jobs=-1
) # 지표는 squared_error로 설정합니다.

forest_rf.fit(X_train[features], Y_train)
features2 = get_important_features(forest_rf, X_train[features], Y_train, threshold=0.7)

display("Feature Importance Method : ", features2)

 

- Adversarial Validation

# train은 0, test는 1로 라벨 변수를 설정합니다.
adv_X_train = holdout_X_train.copy()
adv_X_valid = holdout_X_valid.copy()

adv_X_train['AV_label'] = 0
adv_X_valid['AV_label'] = 1

# 위 두 데이터를 합치고, 셔플합니다.
adv_data = pd.concat([adv_X_train, adv_X_valid], axis=0, ignore_index=True)
adv_data_shuffled = adv_data.sample(frac=1)
adv_X = adv_data_shuffled.drop(['AV_label'], axis=1)
adv_y = adv_data_shuffled['AV_label']

%%time
# Forward Feature Selection
forest_rf = RandomForestClassifier(
    n_estimators=50,
    criterion='log_loss', # log_loss
    random_state=seed,
    n_jobs=-1
)

forest_rf.fit(adv_X[features], adv_y)
features3 = get_important_features(forest_rf, adv_X[features], adv_y, threshold=0.7, upper=False)

display(f"Adversarial Method : {features3}")

# 학습 및 예측을 위한 함수
def train_and_predict(X_train, Y_train, X_valid, Y_valid, X_test, features, random_state):
    gbm = lgb.LGBMRegressor(
        n_estimators=1000,
        random_state=random_state,
        subsample=0.7,
        subsample_freq=1
    )

    gbm.fit(X_train[features], Y_train,
            eval_set=[(X_train[features], Y_train), (X_valid[features], Y_valid)],
            eval_metric='rmse',
            callbacks=[lgb.early_stopping(stopping_rounds=10),
                       lgb.log_evaluation(period=10, show_stdv=True)]
    )

    return gbm.predict(X_test[features])
    
all_preds = []
for selected_features in [features1, features2, features3]:
    preds = train_and_predict(
        holdout_X_train,
        holdout_Y_train,
        holdout_X_valid,
        holdout_Y_valid,
        X_test,
        selected_features,
        42
    )
    RMSE = mean_squared_error(Y_test, preds)**0.5

    display(f"Test rmse : {RMSE}")

    all_preds.append(preds)

# 모든 예측값의 평균을 계산
ensemble_preds = np.mean(all_preds, axis=0)

# 앙상블의 RMSE를 측정
RMSE = mean_squared_error(Y_test, ensemble_preds)**0.5
display(f"Feature Selection Ensemble Test RMSE : {RMSE

# 모든 예측값의 평균을 계산
ensemble_preds = np.mean(all_preds[1:], axis=0)

# 앙상블의 RMSE를 측정
RMSE = mean_squared_error(Y_test, ensemble_preds)**0.5

display(f"Feature Selection Ensemble Test RMSE : {RMSE}")

 

피처 셀력선 전후에 따른 성능을 비교.
Valid RMSE : 4166, 2839, 2801
Test RMSE : 4410, 3417, 3377 ---> 3435
Test RMSE : 3417, 3377 ---> 3384

 

모델 측면의 앙상블 기법

# 1. oofs 생성 부분 추가
# 2. 10개의 다른 seed 사용
oofs = np.zeros(len(X_train))
seeds = [i for i in range(10)] # [0~9]

all_oofs = []
all_total_predicts = []
RMSES = []

for seed in seeds:
    kf = StratifiedKFold(n_splits=5)

    cut_Y_train = pd.cut(Y_train,
                        1000, # 데이터를 최소 최대 구간으로 1000등분 합니다.
                        labels=False)
    train_folds = kf.split(X_train, cut_Y_train)


    seed_oofs = np.zeros(len(X_train))
    seed_predicts = np.zeros(len(X_test))

    for fold_idx, (train_idx, valid_idx) in enumerate(train_folds):
        display(f"-------- Seed {seed}, Fold {fold_idx} --------")
        kfold_X_train = X_train.iloc[train_idx, :][features]
        kfold_Y_train = Y_train[train_idx]
        kfold_X_valid = X_train.iloc[valid_idx, :][features]
        kfold_Y_valid = Y_train[valid_idx]

        gbm = lgb.LGBMRegressor(
            n_estimators=1000,
            random_state=seed,
            subsample=0.7,
            subsample_freq=1
        )

        gbm.fit(kfold_X_train, kfold_Y_train,
                eval_set=[(kfold_X_train, kfold_Y_train), (kfold_X_valid, kfold_Y_valid)],
                eval_metric='rmse',
                callbacks=[lgb.early_stopping(stopping_rounds=10),
                        lgb.log_evaluation(period=10, show_stdv=True)]
        )

        fold_predicts = gbm.predict(X_test[features])
        seed_oofs[valid_idx] = gbm.predict(kfold_X_valid)
        seed_predicts += fold_predicts / n_folds

    # RMSE를 측정
    RMSE = mean_squared_error(Y_test, seed_predicts)**0.5
    display(f"Seed {seed} Ensemble Test RMSE : {RMSE}")

    all_oofs.append(seed_oofs)
    all_total_predicts.append(seed_predicts)
    RMSES += [RMSE]

display("RMSE")
display(RMSES)
kf = StratifiedKFold(n_splits=5)

cut_Y_train = pd.cut(Y_train,
                    1000, # 데이터를 최소 최대 구간으로 1000등분 합니다.
                    labels=False)
train_folds = kf.split(X_train, cut_Y_train)

# 3. Stacking 모델 구현
stacking_train = np.column_stack(all_oofs)
stacking_test = np.column_stack(all_total_predicts)
final_predicts = np.zeros(len(X_test))

for fold_idx, (train_idx, valid_idx) in enumerate(train_folds):

    kfold_X_train = stacking_train[train_idx, :]
    kfold_Y_train = Y_train[train_idx]
    kfold_X_valid = stacking_train[valid_idx, :]
    kfold_Y_valid = Y_train[valid_idx]

    gbm = lgb.LGBMRegressor(
        n_estimators=1000,
        random_state=seed,
        subsample=0.7,
        subsample_freq=1
    )

    gbm.fit(kfold_X_train,
            kfold_Y_train,
            eval_set=[
                 (kfold_X_train, kfold_Y_train),
                 (kfold_X_valid, kfold_Y_valid)
            ],
            eval_metric='rmse',
            callbacks=[
                lgb.early_stopping(stopping_rounds=10),
                lgb.log_evaluation(period=10, show_stdv=True)
            ]
    )

    fold_predicts = gbm.predict(stacking_test)
    final_predicts += fold_predicts
final_predicts = final_predicts / n_folds
RMSE = mean_squared_error(Y_test, final_predicts)**0.5
display(f"Stacking Model Test RMSE: {RMSE}")

 

Holdout 학습의 최종결과.
Training Time : 13.7s
Valid RMSE : 2966.63
Test RMSE : 3457.82

Stacking 학습의 최종결과
Seed별 RMSE : 3274, 3342, 3347, 3244, 3232, 3337, 3293, 3293, 3249, 3294
Test RMSE : 3869.9008310979557
kf = StratifiedKFold(n_splits=5)

cut_Y_train = pd.cut(Y_train,
                    1000, # 데이터를 최소 최대 구간으로 1000등분 합니다.
                    labels=False)
train_folds = kf.split(X_train, cut_Y_train)

# 3. Stacking 모델 구현
stacking_train = np.column_stack(all_oofs)
stacking_train = pd.concat([pd.DataFrame(stacking_train), X_train[features]], axis=1).astype(float)

stacking_test = np.column_stack(all_total_predicts)
stacking_test = pd.concat([pd.DataFrame(stacking_test), X_test[features]], axis=1).astype(float)
final_predicts = np.zeros(len(X_test))

for fold_idx, (train_idx, valid_idx) in enumerate(train_folds):

    kfold_X_train = stacking_train.iloc[train_idx, :]
    kfold_Y_train = Y_train[train_idx]
    kfold_X_valid = stacking_train.iloc[valid_idx, :]
    kfold_Y_valid = Y_train[valid_idx]

    gbm = lgb.LGBMRegressor(
        n_estimators=1000,
        random_state=seed,
        subsample=0.7,
        subsample_freq=1
    )

    gbm.fit(kfold_X_train,
            kfold_Y_train,
            eval_set=[
                 (kfold_X_train, kfold_Y_train),
                 (kfold_X_valid, kfold_Y_valid)
            ],
            eval_metric='rmse',
            callbacks=[
                lgb.early_stopping(stopping_rounds=10),
                lgb.log_evaluation(period=10, show_stdv=True)
            ]
    )

    fold_predicts = gbm.predict(stacking_test)
    final_predicts += fold_predicts
final_predicts = final_predicts / n_folds
RMSE = mean_squared_error(Y_test, final_predicts)**0.5
display(f"Stacking Model Test RMSE: {RMSE}")

 

Holdout 학습의 최종결과
Training Time : 13.7s
Valid RMSE : 2966.63
Test RMSE : 3457.82

Stacking 학습의 최종결과
Seed별 RMSE : 3274, 3342, 3347, 3244, 3232, 3337, 3293, 3293, 3249, 3294
Test RMSE : 3869.9008310979557 ---> 3285.084274013325


랜덤성 측면

# random_state 목록
random_states = list(range(10))

# 각 random_state로 모델을 학습시키고 예측값을 저장
all_preds = []
RMSES = []

for rs in random_states:
    preds = train_and_predict(holdout_X_train, holdout_Y_train, holdout_X_valid, holdout_Y_valid, X_test, features, rs)
    RMSE = mean_squared_error(Y_test, preds)**0.5

    display(f"SEED {rs} Test rmse : {RMSE}")

    all_preds.append(preds)
    RMSES += [RMSE]

# 모든 예측값의 평균을 계산
ensemble_preds = np.mean(all_preds, axis=0)

# 앙상블의 RMSE를 측정
RMSE = mean_squared_error(Y_test, ensemble_preds)**0.5

display(RMSES)
display(f"Seed Ensemble Test RMSE : {RMSE}")

 

Holdout 학습의 최종결과
Training Time : 13.7s
Valid RMSE : 2966.63
Test RMSE : 3457.82

Seed Ensemble 학습의 최종결과
Training Time : 13.7s * 10
Seed RMSE : 3428, 3438, 3396, 3418, 3388, 3381, 3474, 3523, 3429, 3509
Test RMSE : 3262