본문 바로가기
Kaggle 대회

[ASHRAE - Great Energy Predictor III] Model 성능 향상시키기

by 사자처럼 우아하게 2019. 12. 8.

이번에는 제가 Model 성능을 향상 시키기 위해 사용한 방법들을 살펴보겠습니다.(Base : LGBM)

이전 포스팅에서 리뷰했던 Baseline 모델의 성능은 LB = 1.3 입니다.  (Github 코드 : 바로가기)

 

1. Feature Engineering : 1.3 → 1.13

   - 앞선 Baseline에서 활용한 Feature 외에 몇 가지 Feature를 더 만들어서  반영했습니다.

     아래 과정을 통해 LB Score를 0.17 향상시켰습니다.

     첫번째는  isholiday  입니다. 건물의 전력소모량을 예측하는 문제이니 당연히 고려되어야 할 요소가 바로 휴일입니

     다. 휴일에는 Education 이나 Office로 활용하는 건물은 휴일에는 전력 소모량이 줄어들 것이기 때문입니다.

     비슷하게 생각하여 주말도 전력 소모가 적을 것으로 예상되지만 이는 이미 timestamp로 월/주/일을 만들면서 

     weekday(test_df['timestamp'].dt.weekday)를 만들어뒀기 때문에 여기에서는 추가하지 않았습니다.

holidays = ["2016-01-01", "2016-01-18", "2016-02-15", "2016-05-30", "2016-07-04",
                "2016-09-05", "2016-10-10", "2016-11-11", "2016-11-24", "2016-12-26",
                "2017-01-01", "2017-01-16", "2017-02-20", "2017-05-29", "2017-07-04",
                "2017-09-04", "2017-10-09", "2017-11-10", "2017-11-23", "2017-12-25",
                "2018-01-01", "2018-01-15", "2018-02-19", "2018-05-28", "2018-07-04",
                "2018-09-03", "2018-10-08", "2018-11-12", "2018-11-22", "2018-12-25",
                "2019-01-01"]

train_df["is_holiday"] = (train_df.timestamp.dt.date.astype("str").isin(holidays)).astype(int)
test_df["is_holiday"] = (test_df.timestamp.dt.date.astype("str").isin(holidays)).astype(int)

      두번째는 Aggregation 입니다.  Aggregation은 요즘 Kaggle에서 안하는게 이상할 정도로 널리 쓰이고 있는 방법

      으로  특정 변수의 mean,std,max,min 값 등을 변수로 추가하는 방법입니다.  이번에 Aggregation을 weather 데이터

      에 대해서 최근 3일 최근 72일 데이터의 평균/편차/최대/최소값을 site 별로 만들어서 추가해주었습니다.      

def add_lag_feature(weather_df, window=3):
    group_df = weather_df.groupby('site_id')
    cols = ['air_temperature', 'cloud_coverage', 'dew_temperature', 'precip_depth_1_hr', 'sea_level_pressure', 'wind_direction', 'wind_speed']
    rolled = group_df[cols].rolling(window=window, min_periods=0)
    lag_mean = rolled.mean().reset_index().astype(np.float16)
    lag_max = rolled.max().reset_index().astype(np.float16)
    lag_min = rolled.min().reset_index().astype(np.float16)
    lag_std = rolled.std().reset_index().astype(np.float16)
    for col in cols:
        weather_df[f'{col}_mean_lag{window}'] = lag_mean[col]
        weather_df[f'{col}_max_lag{window}'] = lag_max[col]
        weather_df[f'{col}_min_lag{window}'] = lag_min[col]
        weather_df[f'{col}_std_lag{window}'] = lag_std[col]

 

      세번째는 Outlier 제거 입니다. 앞서 EDA 포스팅에서 확인 했듯이 meter=0이고 Building_id<=104인 데이터는

      특정시점까지 모두 Meter_reading 값이 0 이었습니다. 데이터 누락이 의심되므로 이 데이터는 Outlier로 보고

       제거했습니다.

train_df = train_df [ train_df['building_id'] != 1099 ]
train_df = train_df.query('not (building_id <= 104 & meter == 0 & timestamp <= "2016-05-20")')

     네번째는 Categorical Feature 지정 입니다. 이게 소개해드린 방법들 중에서 가장 중요하다고 생각합니다. 카테고리

     변수를 처리해주기 위해서 보통 One-hot encoding 이나 sklearn의 LabelEncoder 함수를 활용해서 사전에 인코딩을

     해두고 접근하는 편인데(적어도 저는 그랬습니다.) LGBM 은 Categorical_feature를 지정해주는 기능이 있습니다.

     이를 지정을 하고 안하고의 성능 차이는 상당히 많이 났습니다. LGBM 공식 문서를 봐도 One-hot encoding을 해서

     입력하는 것보다 categorical features 를 지정하는 것이 optimal split을 찾는데 더 도움이 되고 good accuracy를

     제공 한다고 안내되고 있습니다.

 

     추가로 트리 모델에서 high cardinality categorical features(범주의 갯수가 많은 변수)를 원핫인코딩하여 넣게 되면

    트리가 언밸런스 해지고, 좋은 성능을 내기 위해 트리가 더 깊어진다고 합니다.즉 훈련시키는데도 시간이 더 소요되

    고, 과적합될 위험도 있다는 의미합니다.(참고 : 링크)

categorical_features = ["building_id", "site_id", "meter", "hour", "weekend",'month','is_holiday','primary_use']

 

2. Meter_reading Aggregation by building_id : LB 1.13 → 4.32 

    - 이번에는 제가 욕심을 내다가 실패했던 사례입니다. 저는 단순하게 Building_id 의 1년간 전력소모량을 평균/편차

      내어서 Feature로 만들면(Train,Test 모두) 성능 향상에 도움이 되지 않을까 라는 생각으로 Meter_reading에 대해

      building_id 별로 aggregation을 실시하였고 결과는 처참했습니다.

      학습때는 이상한점을 느끼지 못하고 오히려 학습이 잘된다고 느꼈던것으로 보아 굉장한 Overfit이 되지 않았나 라고

      생각하고 있습니다.

df_group = train_df.groupby('building_id')['meter_reading_log1p']
building_mean = df_group.mean().astype(np.float16)
building_median = df_group.median().astype(np.float16)
building_min = df_group.min().astype(np.float16)
building_max = df_group.max().astype(np.float16)
building_std = df_group.std().astype(np.float16)

train_df['building_mean'] = train_df['building_id'].map(building_mean)
train_df['building_median'] = train_df['building_id'].map(building_median)
train_df['building_min'] = train_df['building_id'].map(building_min)
train_df['building_max'] = train_df['building_id'].map(building_max)
train_df['building_std'] = train_df['building_id'].map(building_std)

 

3. Inference 방법 수정 : LB 1.13 → 1.10

  - Target value 인 Meter_reading 값은 한쪽으로 skew 되어 있는 값이다보니 보통 Log를 씌워 학습한 후에 제출할 때  

    exp 함수를 씌워 원복 시킵니다. 처음에는 별생각없이 Cross validation 한 결과물 (folds= 3 )을 평균 낸 후에 산출된

    결과물에 exp함수를 씌웠는데 이렇게 하면 우리가 최소화하고자 하는 Loss function이 바뀌어 버리는 문제가 있음을

    발견하고 코드를 수정했습니다. (exp 함수 후 평균 ) 그 결과 0.03 향상 되었습니다.

    

%%time
i = 0
res = []
res2 = []
res3 = []
step_size = 50000
for j in tqdm(range(int(np.ceil(test_df.shape[0] / 50000)))):
     # 평균을 내고 나서 exp를 취하는 코드
    res.append(np.expm1(sum([model.predict(test_df.iloc[i:i + step_size]) for model in models]) / folds))
    # exp를 취하고 평균을 내는 코드 https://www.kaggle.com/rohanrao/ashrae-half-and-half
    res2.append(sum([np.expm1(model.predict(test_df.iloc[i:i + step_size])) for model in models])/ folds)
    
    i += step_size

 

4. 기타 : LB 1.10 → 1.09

   - skew 되어 있던 Square feet에 Log를 취하는 것과 weater data 의 Null 값을  site별로 채워주는 코드를 추가하여

     0.01 만큼 향상시켰다. 이때 공개 커널에서 사용되는 make_is_bad_zero 함수를 이용하여 약 3%의 Bad rows를 

     제거 하고 학습 시켰다. 

 

 

* Reference

   - https://www.kaggle.com/aitude/ashrae-kfold-lightgbm-without-leak-1-08

   - https://www.kaggle.com/rohanrao/ashrae-half-and-half

   - https://www.kaggle.com/ragnar123/another-1-08-lb-no-leak

    - https://github.com/Microsoft/LightGBM/blob/master/docs/Features.rst#optimal-split-for-categorical-features

 

 

 

 

댓글