Oct 20, 2018 - Recommend System Fm(draft)

factorization machine

이번에 프로젝트하면서 공부를 해야하는데 한글 문서는 거의 전무했고 제대로 정리된 문서도 논문 밖에 없었다. 논문으로 이론적인 이해를 하더라도 실행 코드는 논문에서 다루지 않아 따로 공부해야하는데 이 또한 문서가 빈약하다고 느꼈다.

이론은 나보다 잘 아는 사람이 많겠으니, 조만간 정리해서 소스 코드 위주로 튜토리얼을 만들어보자.

Oct 13, 2018 - Manipulating Big Data

python memory efficiency (manipulating big data)

9월 말부터 지금까지 추천 관련 업무를 하느라 바쁘게 지내서 오랫만에 정리글을 쓴다. (지난 글도 아직 정리가 더 필요한데…) 이번 기회에 추천시스템에서 자주 보이는 MF/FM/FFM을 써봤고, 빅데이터라고 불릴 정도의 데이터를 다뤄보았다. 현재 작업한 양은 샘플 데이터임에도 회사 서버 메모리로 감당이 안되는 수준이다보니 작업하는데 애로사항이 많았다.

spark를 쓰는 게 제일 좋을 것 같지만, spark를 외면하며 준비한 (pandas에 익숙한 본인을 기준으로) 쓰기 쉽고 효과적인 memory error 해결법을 소개한다.

pandas memory error 해결법

  1. dtype 변환
  2. chunksize
  3. dask 사용
  4. modin.pandas 사용 회사 서버에 인스톨이 안됨

몇가지 핵심적인 사항만 짚어보자.

  • dtype 변환
    1. 데이터가 메모리 사이즈보다 작지만, 원활한 전처리가 안될 때 시도.
    • 생각보다 pandas에서 reshape나 save 기능이 먹는 메모리가 상당하여 4GB 데이터인데도 16GB 램이 모자랄 수 있는데, 그럴 때 dtype 변환만으로 해결할 수 있다
      1. category 를 적극 활용할 것.
    • 특히, category는 범주형 데이터 전처리에서 여러모로 좋으니 왠만하면 category 설정은 하자.
      1. int64int16, int8등 작은 크기로 바꿀 것.
    • get_dummies에서 쓰는 uint는 음수가 아닌 양의 정수 타입을 뜻함.
  • chunksize
    1. read_csv(..., chunksize=1000) iterator 객체 형태로 데이터를 쓸 수 있다.
    2. 고용량 text에서 간단히 샘플링할 때 쉽고 간편.
    3. ..to_csv(..., chunksize=1000) 1000개씩 끊어서 저장하기 때문에 작업에 걸리는 램 사용량이 굉장히 줄어들지만 성능이 비교적 느리다.
  • dask
    1. pandas와 유사해 사용법이 매우 쉬워보이지만, 생각보다 러닝 커브가 있음.
    2. 일정 수준보다 npartition을 높게 잡아줘야하고, Client를 써야 성능을 제대로 낼 수 있을 것.
    3. dask tutorial.

추가: parquet 자료형 추천한다. csv보다, pickle보다 용량/성능/호환성 면에서 나쁜 것도 없고 좋은 점이 더 많다. 특히 용량면에서는 sparse matrix의 경우 pickle의 1% 크기여서 파일이 잘못된 건지 체크할 정도.

Sep 16, 2018 - Understanding How To Use Xgboost

xgboost

프로젝트에서 주로 사용했던 xgboost(v72) 알고리즘을 깊이 이해하고자 정리했다.

  • parameters
    • n_jobs/nthread: parallel computing
    • booster: gbtree/gblinear/dart
    • objective: reg:linear etc
    • tree specifing args: max_dept/min_child_weight
    • gamma: hessian sum threshold which determine tree spliting point
    • alpha: lasso regression parameter
    • lambda: ridge regression parameter
    • tree_method: hist, exact, gpu_hist
  • args
    • early_stop: eval_set/feval으로 num_boost_round validation 과정
    • eval_set: train/test set 설정
    • feval: eval_set에 적용할 metric

performance

최적 모델을 뽑기 위해선 gridsearch와 같은 파라미터 튜닝이 필수다. 당연히 계산량이 상당히 많아서 실행속도를 줄여주는 파라미터에 관심이 갈 수 밖에 없다.

  • n_jobs cpu version xgboost wrapper(XGBRegressor)를 사용할 때 사용할 core 개수. 주의할 점은 -1로 설정할 경우 모든 core를 사용하는데 이미 core에 할당된 작업이 있으면 그 작업이 끝날 때까지 기다리므로 상당히 오래 걸릴 수 있다.

  • nthread xgboost wrapper가 아닌 xgboost의 본래 함수에서 쓰는 arg. xgboost.cv와 gridsearch 함수를 돌린다면 n_jobs와 nthread를 둘 다 사용해야 cpu 점유율이 정상적으로 상승하는 것을 확인했다. cpu 점유율을 보면 n_jobs와 세부적 작동 방식과 다른 것 같지만 잘 모르겠다.

  • tree_method hist 방식이 체감상 가장 빠르다. gpu_hist 사용시 hist보다 약 6배 빨라진 것을 확인했다.

not mentioned explictly on doc

  • parameter tuning 경험상 가장 중요한 parameter는 max_dept, min_child_weight, num_boost_round 3가지 정도로 생각한다. 나머지는 드라마틱한 변화가 없는 편이니 튜닝보다는 feature engineering을 더 보는 게 성능이 좋다. 고려할 순서는
    1. y값 transform ex) sqrt, box-cox
    2. x값 transform ex) sqrt, box-cox
    3. x값 generate ex) x3 = x1/x2, x1*x2, x1^x2
  • booster gbtree은 tree based model로 XGBRegressorgbtree를 쓰기 때문에 엄밀히 regression은 아니다. 다만 objectivereg:linear로 사용하기 때문에 continuous y의 error를 계산해준다. 정확한 formula는 알지 못하나, 대강 MSE형태를 쓰는 것으로 보인다.

    gbliner이 우리가 흔히 아는 regression 형태다. 실제로 predict를 해보면 gblinear은 피처와 증감 방향과 동일하게 움직이나, gbtree에선 monotonous가 아니다.

  • gamma 트리가 분기되는 지점을 결정할 때, obective function 2차 미분값의 합이 gamma보다 커야한다는 의미다. 그래서 목적 함수를 바꾸면 gamma값을 바꿔야 한다.

  • NaN 학습시 target variable에 NaN이 있으면 validation error를 구하지 못해 차질이 있으나, predictor의 NaN은 학습에 지장이 없으며 학습된 트리의 노드를 살펴보면 NaN은 부등식의 왼쪽 방향으로(조건식 True방향) 분류된다.

  • objective 프로젝트에서 예측의 MAPE도 굉장히 중요한 지표여서 튜닝의 최종단계에서 objective function을 수정해봤다. 기본값도 성능이 좋은 편이어서 굳이 수정할 필요는 없다. 수정을 할 땐 이런 함수를 작성해야하는데
    def my_custom( preds, dtrain ):
      labels = dtrain.get_label()
      grad = # my formula 1st derivative
      hess = # my formula 2nd derivative
      return grad, hess
    

    미분식을 구하기가 불편하다. 그럴 때 미분계산기를 이용하자.

  • early_stop eval_set(train/validation error)으로 early stop을 수행한다. 여기서 나온 값이 num_boost_round가 되고 XGBRegressor라면 n_estimator가 된다. validation error는 eval_metric이란 argument로 지정할 수 있는데, 만약 MAPE와 같은 cumstom error function을 원한다면 f_eval argument로 다음과 같은 형태 함수를 지정한다.
    def my_custom(preds, dtrain):
      labels = dtrain.get_label()    
      return 'error', mean_absolute_error(labels, preds)
    

gridsearch

sklearn을 사용하여 여러번 파라미터 튜닝을 했는데, 잘 짜인 gridsearch는 적당한 값을 빠르게 찾아준다는 점에서 randomized search보다 낫다. 아래 사용했던 소스를 올렸으며, 소스의 원안은 포스팅을 참조


def custom_loss(pred, dtrain):
    true = dtrain.get_label()
    grad = ...
    hess = ...
    return grad, hess

custom_eval = make_scorer(..., greater_is_better=False)

# initialize parameter
par = {'colsample_bytree': 0.8, 'gamma': 0,
       'learning_rate': 0.1, 'max_depth': 5,
       'min_child_weight': 1, 'n_estimators': 1050,
       'silent': True, 'subsample': 0.8, 'tree_method': 'hist',
       'nthread': 5, 'n_jobs': 5,
       }
	   
# step1: temporary num boost
early_stop = xgboost.cv(par, dtrain, 5000, nfold=5, early_stopping_rounds=50, feval=eval_rwmse, obj=custom_loss)
num_boost = early_stop.index.max()
par['n_estimators'] = num_boost

# step2: max_depth/min_child_weight
param_test1 = {
    'max_depth': range(5, 9, 1),
    'min_child_weight': range(1, 4, 1)
}

grid_search_tree = GridSearchCV(estimator=xgboost.XGBRegressor(**par, objective=obj_rwmse_2), param_grid=param_test1,
                                scoring=custom_eval,
                                iid=False, cv=4, return_train_score=False)
grid_search_tree.fit(x_train.drop('예상취급액', axis=1), x_train.예상취급액)
print(pd.DataFrame.from_dict(grid_search_tree.cv_results_))
param_update = grid_search_tree.best_params_

# step3: gamma
for x in param_update:
    par[x] = param_update[x]
param_test3 = {
    'gamma': [i / 10.0 for i in range(0, 5)]
}
grid_search_gamma = GridSearchCV(estimator=xgboost.XGBRegressor(**par, objective=obj_rwmse_2), param_grid=param_test3,
                                 scoring=custom_eval,
                                 iid=False, cv=4, return_train_score=False)
grid_search_gamma.fit(x_train.drop('예상취급액', axis=1), x_train.예상취급액)
print(pd.DataFrame.from_dict(grid_search_gamma.cv_results_))
param_update = grid_search_gamma.best_params_

# step4: update num boosting
for x in param_update:
    par[x] = param_update[x]
early_stop = xgboost.cv(par, dtrain, 5000, nfold=5, early_stopping_rounds=50, feval=eval_rwmse, obj=custom_loss)
num_boost = early_stop.index.max()

# step5: tune randomness
par['n_estimators'] = num_boost
param_test4 = {
    'subsample': [i / 10.0 for i in range(7, 10)],
    'colsample_bytree': [i / 10.0 for i in range(7, 10)]
}
grid_search_random = GridSearchCV(estimator=xgboost.XGBRegressor(**par, objective=obj_rwmse_2), param_grid=param_test4,
                                  scoring=custom_eval,
                                  iid=False, cv=4, return_train_score=False)
grid_search_random.fit(x_train.drop('예상취급액', axis=1), x_train.예상취급액)
print(pd.DataFrame.from_dict(grid_search_random.cv_results_))
param_update = grid_search_random.best_params_

# step6: tune regularization
for x in param_update:
    par[x] = param_update[x]
param_test7 = {
    'reg_alpha': [4.2, 4.3],
    'reg_lambda': [4.75, 5, 7, 9]
}
grid_search_regular = GridSearchCV(estimator=xgboost.XGBRegressor(**par, objective=obj_rwmse_2), param_grid=param_test7,
                                   scoring=custom_eval,
                                   iid=False, cv=4, return_train_score=False)
grid_search_regular.fit(x_train.drop('예상취급액', axis=1), x_train.예상취급액)
print(pd.DataFrame.from_dict(grid_search_regular.cv_results_))
param_update = grid_search_regular.best_params_

# step7: reduce learning rate (or can use cv) 
for x in param_update:
    par[x] = param_update[x]
par['learning_rate'] = 0.015
final_model = xgboost.cv(par, dtrain, 10000, nfold=5, early_stopping_rounds=50, feval=eval_rwmse, obj=custom_loss)
par['n_estimators'] = final_model.index.max()
final_par = par
print('final params')
print(final_par)

dart

간단히 비교 모델로 테스트해봤는데 딥러닝에서 쓰는 drop out이 가미된 xgboost라고 보면 될 것 같다.
결과만 따지면 예측력이 떨어졌는데, 그 이유는 내가 사용한 데이터가 row 개수가 적은 반면 동일한 X input값에 걸린 y 값의 분산이 상당히 크므로 학습이 까다로운 편이다.
validation curve를 그려보면 boost를 아무리 많이 해도 overfitting로 발생하는 test error 상승이 없다. 왠만하면 많이 돌릴수록 cv error가 낮다.
분석 경험을 토대로 왜 그런 현상이 나왔는지 추측해보면, 1.대부분의 학습력은 초기 ~100 round에서 가져가고 2. 트리를 열어보면 ~3 depth에서 중요한 분류는 거진 일어나는 것 같다. 3. 나머지 자투리 학습을 수많은 weak learner가 매꿔주는데 그 weak learner가 어마어마하게 필요할 만큼 complex한 규칙이 있는 것으로 보인다. 4. tresh in, tresh out이라고. 사실 데이터 자체가 답이 안 나오는 셋이 아닐까.. 라는 생각도 크다.
drop out으로 트리를 지울 때 그게 만약 예측에서 중요한 트리였다면 당연히 성능에 안 좋을 거고, 그게 아닌 미미한 파트였다면 사실 지우나 마나할 것이라고 생각한다. 그래서 drop out이 특징인 dart는 오히려 성능이 떨어지고 XGBRegressor의 n_estimator를 올려 overfitting에 가까울수록 성능이 잘 나왔다고 생각한다.