AI/빅콘테스트 2020 챔피언리그

[2] 판매실적 예측_2. DeepFM, xDeepFM 최적화

eraser 2020. 8. 31. 01:54
반응형

 공모전의 첫 번째 문제인 판매실적 예측을 위해 모델링을 진행했다. 예측한 판매실적에 대한 성능은 MAPE로 측정한다. 

 

 

 


 

# Bayesian Optimization 

 

 DeepFM, xDeepFM 모델의 학습 과정에서 hyperparameter의 최적값을 설정하기 위해 베이지안 최적화 방법을 사용했다. 

 

베이지안 최적화 원리 (출처: https://github.com/fmfn/BayesianOptimization)

 

 파란색 선이 찾으려고 하는 목적함수라고 하자. 무엇인지 알려져 있지 않다. 검정색 선은 관측한 데이터를 바탕으로 예측한 목적함수이다. 알려져 있지 않은 실제의 목적함수가 존재할 만한 영역에서, Acquisition Function(아래의 그래프)에서 더 큰 값을 가지도록 하는 영역을 확률적으로 찾아 가며 최적화한다.

 


 

# 사용 라이브러리

 

  • BayesianOptimization : 베이지안 최적화를 사용할 수 있도록 구현해 놓은 오픈소스 라이브러리이다.
  • deepctr : 판매실적 예측을 위해 진행한 모델링에서 사용한다.
  • functools : partial 함수를 사용하기 위해 사용한다.
  • 그 외: scikit-learn, pandas 등

# 코드

 

 라이브러리를 사용하는 데 있어 핵심은 모델에서 BayesianOptimization 객체를 생성하는 것, 최대화 과정을 수행하는 것이다.

 

 

1. BayesianOptimization 객체

 

 BayesianOptimization 객체는 크게 두 가지를 파라미터로 받는다. 첫째는 탐색 대상 함수, 둘째는 탐색 대상 함수에서 탐색할 하이퍼파라미터의 집합 및 그 범위이다. 

 탐색 대상 함수는 모델을 반환한다. 그리고 탐색 과정에서 모델 내 하이퍼파라미터가 바뀌어야 하기 때문에, 함수 내에서 인자 값을 바꿀 수 있도록 모델을 반환하는 함수를 partial 함수로 감싼(?) 후 BayesianOptimization 객체에 전달한다.

 다음으로 최적화할 하이퍼파라미터의 조합 및 탐색할 대상을 설정한다. 최적화할 하이퍼파라미터는 당연한 말이지만 모델 네트워크를 구성할 때 알 수 있다. 판매실적 예측을 위해 사용한 최종 모델이 xDeepFM 모델이었기 때문에, 해당 모델의 파라미터를 기준으로 코드를 기록한다.

 

 첫째, 나는 하이퍼파라미터를 가변 길이의 키워드 인자로 전달하여, 그것을 함수 내부에서 모델에 전달하도록 했다. 다만, 지금 코드를 보니 상당히 가독성이 떨어지는 듯하다.

 둘째, 함수 내에서 에폭 수만큼 훈련을 한 뒤, MAPE 스코어를 반환하도록 했다. 모델만 바로 반환하면 안 되고, 훈련까지 진행한 뒤 가장 좋은 모델을 찾고, 그 모델에서 하이퍼파라미터를 바꾼 뒤 또 제일 좋은 모델을 찾도록 해야 한다는 생각에서였다. 그런데, 이것도 지금 생각하니 맞는 접근인지 모르겠다. 전에 한창 코드 짤 때에는 소스를 찾기가 어려웠어서 이렇게 했었는데, 모델만 반환하는 것이 맞는지 다시 고민해봐야 겠다. 한편, 최대화를 통해 하이퍼파라미터를 탐색하므로, 반환하는 score 값의 부호를 반대로 만들어 주었다.

 셋째, 하이퍼파라미터로는 바꿀 수 있는 것을 최대한 다 생각해서 경우의 수로 전달했다. 즉, xDeepFM 모델 뿐만 아니라, optimizer의 learning rate, batch size 등까지 모두 같이 전달했다는 의미이다. 바로 위에서 말한 것처럼 학습하는 것까지 모두 제어하고, 최적의 weight를 찾는 과정까지 베이지안 최적화로 진행하고 싶었기 때문인데, 최적화 과정에 시간이 너무 오래 걸려 이 부분까지는 하이퍼파라미터 탐색 과정에 넣지 못했다. 그런데 지금 다시 보니, 바로 위에서 든 의문과 같은 의미에서, 이렇게 하는 게 맞는 건가 하는 의구심이 든다.

 

 

# 모듈 불러오기
from deepctr.models.xdeepfm import xDeepFM
from bayes_opt import BayesianOptimization
from functools import partial
...

# 탐색할 목적함수로 네트워크 설정 후 훈련을 통해 weight 설정
def train_and_validate_xdeepfm(linear_feature_columns, dnn_feature_columns, train_ds, train_label, test_ds, test_label, **kwargs):

    # 하이퍼파라미터 전달 
    dnn_hidden_units = int(kwargs.pop('dnn_hiden_units', 256))
    if dnn_hidden_units % 2:
        dnn_hidden_units += 1
    cin_layer_size = int(kwargs.pop('cin_layer_size', 128))
    if cin_layer_size % 2:
        cin_layer_size += 1
    cin_split_half = kwargs.pop('cin_split_half', True)
    cin_activation = kwargs.pop('cin_activtion', 'relu')
    l2_reg_linear = kwargs.pop('l2_reg_linear', 1e-05)
    l2_reg_embedding = kwargs.pop('l2_reg_embedding', 1e-05)
    l2_reg_dnn = kwargs.pop('l2_reg_dnn', 0)
    l2_reg_cin = kwargs.pop('l2_reg_cin', 0)
    seed = kwargs.pop('seed', 1024)
    dnn_dropout = kwargs.pop('dnn_dropout', 0)
    dnn_activation = kwargs.pop('dnn_activation', 'relu')
    dnn_use_bn = kwargs.pop('dnn_use_bn', True)

    # 모델 구성
    model = xDeepFM(linear_feature_columns, dnn_feature_columns,
                    dnn_hidden_units=(dnn_hidden_units, dnn_hidden_units),
                    cin_layer_size=(cin_layer_size, cin_layer_size),
                    cin_split_half=cin_split_half,
                    cin_activation=cin_activation,
                    l2_reg_linear=l2_reg_linear,
                    l2_reg_embedding=l2_reg_embedding,
                    l2_reg_dnn=l2_reg_dnn,
                    l2_reg_cin=l2_reg_cin,
                    seed=seed,
                    dnn_dropout=dnn_dropout,
                    dnn_activation=dnn_activation,
                    dnn_use_bn=dnn_use_bn,
                    task='regression')

    learning_rate = kwargs.pop('lr', 0.001)
    num_epochs = kwargs.pop('epochs', 300)
    batch_size = kwargs.pop('batch_size', 256)

    model.compile(loss='mape', optimizer=optimizers.Adam(lr=learning_rate))

    # 훈련
    hist = model.fit(train_ds, train_label,
                     batch_size=batch_size,
                     verbose=1,
                     validation_split=0.2)

    # 평가지표 산식
    def __neg_mean_absolute_percentage_error(y_true, y_pred):
        return -np.mean(np.abs((y_true - y_pred) / y_true)) * 100

    test_pred = model.predict(test_ds)
    score = __neg_mean_absolute_percentage_error(test_label['AMT'], test_pred.reshape(-1, ))
    print("Test Set MAPE Score: ", -score)

    return score

 


2. 최대화 과정 수행

 

 생성한 BayesianOptimization 객체에 대해 maximize 메소드를 실행하여 최대화 과정을 수행한다. 인자로 주어지는 것들은 다음과 같다.

  • init_points: 초기 random search 개수.
  • n_iter: 반복 횟수. 즉, 몇 개의 입력값-함숫값 점들을 확인할 것인지이다. 많을수록 더 정확한 값을 얻을 수 있다.
  • acq: 베이지안 최적화 과정에서의 Acquisition Function.
  •  xi: exploration 강도.

 최대화 과정을 수행한 뒤, 찾은 하이퍼파라미터 값을 확인하면 된다. 그 하이퍼파라미터를 가지고, 모델을 학습하면 된다.  나는 이 과정에서, 최적의 optimization 객체를 함수 안에 넘겨서 최적의 모델을 학습하도록 했는데, 지금 보니 코드가 매우 비효율적으로 보인다.

 

# 최대화 과정 수행
optimizer.maximize(init_points=10, n_iter=10, acq='ei', xi=0.01)

# 최적의 모델 학습
def test_best_model_xdeepfm(optimizer, linear_feature_columns, dnn_feature_columns, train_ds, train_label, test_ds, test_label):
    params = optimizer.max['params'] # 왜 이렇게 했을까?

    dnn_hidden_units = int(params.pop('dnn_hiden_units', 256))
    if dnn_hidden_units % 2:
        dnn_hidden_units += 1
    cin_layer_size = int(params.pop('cin_layer_size', 128))
    if cin_layer_size % 2:
        cin_layer_size += 1
    cin_split_half = params.pop('cin_split_half', True)
    cin_activation = params.pop('cin_activtion', 'relu')
    l2_reg_linear = params.pop('l2_reg_linear', 1e-05)
    l2_reg_embedding = params.pop('l2_reg_embedding', 1e-05)
    l2_reg_dnn = params.pop('l2_reg_dnn', 0)
    l2_reg_cin = params.pop('l2_reg_cin', 0)
    seed = params.pop('seed', 1024)
    dnn_dropout = params.pop('dnn_dropout', 0)
    dnn_activation = params.pop('dnn_activation', 'relu')
    dnn_use_bn = params.pop('dnn_use_bn', True)

    lr = params['lr']
    del params['lr']

    best_model = xDeepFM(linear_feature_columns, dnn_feature_columns,
                         dnn_hidden_units=(dnn_hidden_units, dnn_hidden_units),
                         cin_layer_size=(cin_layer_size, cin_layer_size),
                         cin_split_half=cin_split_half,
                         cin_activation=cin_activation,
                         l2_reg_linear=l2_reg_linear,
                         l2_reg_embedding=l2_reg_embedding,
                         l2_reg_dnn=l2_reg_dnn,
                         l2_reg_cin=l2_reg_cin,
                         seed=seed,
                         dnn_dropout=dnn_dropout,
                         dnn_activation=dnn_activation,
                         dnn_use_bn=True,
                         task='regression')

    best_model.compile(loss='mape', optimizer=optimizers.Adam(lr=lr))

    es = EarlyStopping(monitor='val_loss', patience=10, verbose=1)
    hist = best_model.fit(train_ds, train_label,
                          epochs=1000,
                          batch_size=256,
                          validation_split=0.2,
                          shuffle=True,
                          verbose=1,
                          callbacks=[es])

    plt.plot(hist.history['loss'], label='Train Loss')
    plt.plot(hist.history['val_loss'], label='Validation Loss')
    plt.xlabel('epochs')
    plt.ylabel('loss')
    plt.title('Model Train-Validation Loss')
    plt.show()

    def __mean_absolute_percentage_error(y_true, y_pred):
        return np.mean(np.abs((y_true - y_pred) / y_true)) * 100

    test_pred = best_model.predict(test_ds)
    score = __mean_absolute_percentage_error(test_label['AMT'], test_pred.reshape(-1, ))
    print("Test Set MAPE: ", score)

    return best_model, score
    
# 최적의 모델을 찾아 예측 후 가중치 저장
model, score = test_best_model(optimizer, linear_feature_columns, dnn_feature_columns, X_train, y_train, X_test, y_test)
os.makedirs('./models/xDeepFM', exist_ok=True)
model.save(f'./models/xDeepFM/{score}.h5')

 

 


 

 기록하기 민망할 만큼 결과가 좋지 않았고, 무엇보다 탐색 과정에 시간이 너무 오래 걸렸다. 지금 다시 코드를 리뷰하다 보니, 탐색할 목적 함수에 훈련 과정을 넣어 놓은 것, 탐색할 하이퍼파라미터의 수와 범위가 너무 많았던 것이 원인이 아닐까 싶다. 베이지안 최적화에 대해 이론적으로 더 공부하고, 라이브러리 소스코드 구성을 살펴보았더라면 개념적으로 어떻게 코드를 짜야 했을지 더 확신을 가질 수 있었을 것 같아 아쉽다.

 (물론) 결과가 좋았더라면 1차를 통과했겠지만, 어쨌든 베이지안 라이브러리를 사용해 보았음에 의의를 두고 과정을 기록한다. 

 

반응형