factorization machine
이번에 프로젝트하면서 공부를 해야하는데 한글 문서는 거의 전무했고 제대로 정리된 문서도 논문 밖에 없었다. 논문으로 이론적인 이해를 하더라도 실행 코드는 논문에서 다루지 않아 따로 공부해야하는데 이 또한 문서가 빈약하다고 느꼈다.
이론은 나보다 잘 아는 사람이 많겠으니, 조만간 정리해서 소스 코드 위주로 튜토리얼을 만들어보자.
이번에 프로젝트하면서 공부를 해야하는데 한글 문서는 거의 전무했고 제대로 정리된 문서도 논문 밖에 없었다. 논문으로 이론적인 이해를 하더라도 실행 코드는 논문에서 다루지 않아 따로 공부해야하는데 이 또한 문서가 빈약하다고 느꼈다.
이론은 나보다 잘 아는 사람이 많겠으니, 조만간 정리해서 소스 코드 위주로 튜토리얼을 만들어보자.
9월 말부터 지금까지 추천 관련 업무를 하느라 바쁘게 지내서 오랫만에 정리글을 쓴다. (지난 글도 아직 정리가 더 필요한데…)
이번 기회에 추천시스템에서 자주 보이는 MF/FM/FFM을 써봤고, 빅데이터라고 불릴 정도의 데이터를 다뤄보았다. 현재 작업한 양은 샘플 데이터임에도 회사 서버 메모리로 감당이 안되는 수준이다보니 작업하는데 애로사항이 많았다.
spark를 쓰는 게 제일 좋을 것 같지만, spark를 외면하며 준비한 (pandas에 익숙한 본인을 기준으로) 쓰기 쉽고 효과적인 memory error 해결법을 소개한다.
몇가지 핵심적인 사항만 짚어보자.
category
를 적극 활용할 것.int64
를 int16
, int8
등 작은 크기로 바꿀 것.get_dummies
에서 쓰는 uint
는 음수가 아닌 양의 정수 타입을 뜻함.read_csv(..., chunksize=1000)
iterator 객체 형태로 데이터를 쓸 수 있다...to_csv(..., chunksize=1000)
1000개씩 끊어서 저장하기 때문에 작업에 걸리는 램 사용량이 굉장히 줄어들지만 성능이 비교적 느리다.npartition
을 높게 잡아줘야하고, Client
를 써야 성능을 제대로 낼 수 있을 것.추가: parquet 자료형 추천한다. csv보다, pickle보다 용량/성능/호환성 면에서 나쁜 것도 없고 좋은 점이 더 많다. 특히 용량면에서는 sparse matrix의 경우 pickle의 1% 크기여서 파일이 잘못된 건지 체크할 정도.
프로젝트에서 주로 사용했던 xgboost(v72) 알고리즘을 깊이 이해하고자 정리했다.
최적 모델을 뽑기 위해선 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배 빨라진 것을 확인했다.
booster
gbtree
은 tree based model로 XGBRegressor
도 gbtree
를 쓰기 때문에 엄밀히 regression은 아니다.
다만 objective
를 reg: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방향) 분류된다.
def my_custom( preds, dtrain ):
labels = dtrain.get_label()
grad = # my formula 1st derivative
hess = # my formula 2nd derivative
return grad, hess
미분식을 구하기가 불편하다. 그럴 때 미분계산기를 이용하자.
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)
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)
간단히 비교 모델로 테스트해봤는데 딥러닝에서 쓰는 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에 가까울수록 성능이 잘 나왔다고 생각한다.