데이콘과 토스가 같이 주최한 광고 CTR 예측 경진대회에 참여했다.
https://dacon.io/competitions/official/236575/overview/description
토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 모델 개발 - DACON
분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.
dacon.io
추천 스터디에서 인연을 맺은 두 분과 대회에 참여했다. 그 중 한 분의 캐글 티어가 Expert이셨고 이 분의 하드 캐리 덕에...

Private 점수 기준 대략 700팀 중에 5등이라는 좋은 성적을 거두었다. 솔루션은 아래 깃헙 레포에 공개했다. (많관부! 스타 필수!)
https://github.com/ds-wook/toss-next-challenge-solution
GitHub - ds-wook/toss-next-challenge-solution: 토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 대회 5등 모델 제출용
토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 대회 5등 모델 제출용 레포지토리 - ds-wook/toss-next-challenge-solution
github.com
이 글을 통해 ML 경진 대회에 참여하면서 나의 역할은 무엇이었는지, 느꼈던 점, 부족한 점 등 전체적인 회고를 해보고 이를 통해 다음 경진 대회를 미리 준비하고자 한다. (ML을 하는 사람으로서 캐글 Expert는 달아보고 싶은 욕심이.. 생김ㅋㅋ)
대회 개요
토스 서비스 내에서 발생하는 유저의 광고 클릭 데이터를 기반으로 CTR 예측 모델을 설계하는 것이 대회 목표이다. Y는 1/0으로 구성되며 광고 클릭 데이터이기 때문에 1의 비율이 1%정도 되는, 극도의 불균형 데이터이다. 이 대회에서 제공되는 데이터는 익명화되어 있다. 일부 컬럼은 gender, age로 변수의 의미를 알 수 있었으나 age가 1~8의 정수로 인코딩 되어 있고 1이 어떤 나이대를 의미하는지 주최측에서 알려주지 않았다. 이러한 변수들 외에, 다른 대부분의 변수들은 l_feat_14와 같은 이름, scaling, noising 등으로 익명화되어 있어서 변수의 의미를 통해 feature engineering을 하는 것이 사실상 불가능 했다.
대회 전략
캐글 엑스퍼트 분의 지휘 아래, 우리 팀은 최종 제출물로 부스팅과 딥러닝 모델의 앙상블 형태를 생각했다. 이 두 모델을 앙상블할 때, 경진대회에서 좋은 성능이 나왔던 경험에 근거한다. 또한, 데이터가 익명화되어 있다는 것은 다시 말해서 noise가 섞여 있다는 뜻으로 해석할 수 있다. 따라서, feature engineering의 방향이 새로운 의미를 가지는 변수를 만드는 것이 아니라 denoise 하는 방향으로 진행 되었다. 딥러닝 모델의 경우, 이런 피쳐를 사용하지는 않았으나 부스팅 모델은 denoise된 피쳐를 사용할 때, LB가 좋아짐을 팀원 분께서 확인했다. 또한 세 명이서 협업을 하기 때문에, Jupyter Notebook 보다 Git이 코드를 관리하는데 더 적합하다고 판단했다. 이전에 경진대회에 찍먹 했을 때, Jupyter Notebook으로 작업했었는데 Git으로 협업을 해도 될까 라는 걱정이 조금 들었으나, 역시 Git은 신이었다 (ㅋㅋㅋ). Git을 통해 서로의 코드를 투명하게 관리하고 PR Review을 통해 실수를 재확인했으며 feature engineering 함수 등을 재활용할 수 있었다. 단점은 없었고 장점만 있었다.
나의 역할
앞서 언급 했듯이, 우리 팀은 최종 제출물로 부스팅과 딥러닝 모델의 앙상블을 목표로 했다. 캐글 엑스퍼트 분이 부스팅 모델 전문가이시기 때문에, 필자는 자연스럽게 딥러닝 모델을 설계하는 역할을 맡았다.
설계한 모델
필자는 실험을 좋아한다. 그것도 가장 기본적인 모델에서부터 시작하는 실험. 1/0 분류 문제에서 가장 기본이 되는 모델은 무엇일까? 로지스틱 회귀 모형이다. 필자는 CTR 딥러닝 모델을 설계하기 전에 가장 기본이 되는 로지스틱 회귀 모형부터 실험을 진행했다. 그 후로 deepfm, xdeepfm, fibinet, ffm, dcn, dcn_v2을 설계하여 실험을 진행했다. 모델의 구현 코드는 아래 Github 레포를 참고하자. 사실 지금 와서 생각해보면 성능을 내는 대회에서 로지스틱 회귀부터 설계할 필요가 있었을까 라는 생각이 든다. 관련된 내용은 밑의 부족한 점 챕터에서 자세하게 살펴보자.
https://github.com/ds-wook/toss-next-challenge-solution/tree/main/src/models/fm
toss-next-challenge-solution/src/models/fm at main · ds-wook/toss-next-challenge-solution
토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 대회 5등 모델 제출용 레포지토리 - ds-wook/toss-next-challenge-solution
github.com
문제 해결 사례
대회에 참여하는 내내, 익숙하지 않은 문제와 고민의 연속이었다. 클로드의 도움 덕분에! 많은 문제를 해결했는데 이를 기록하고자 한다.
Validation 대회 점수가 LB의 너무 큰 차이
대회 초반에 필자는 validation 대회 점수와 LB가 너무 커서 당황했다. 예를 들어서 validation 대회 점수는 0.32인데 LB는 0.22인 식이다. 이 대회에서 0.01이라는 점수는 매우 큰 점수 차이이다. 처음에는 모델의 성능이 좋지 않겠지 라고 생각했는데 로지스틱 회귀 -> 전통 FM으로 넘어가도 비슷한 현상이 발생해서 leakage가 발생하지 않았는지, 코드에서 실수가 있었는지 다시 살펴봤다.
결론적으로, pytorch test_dataloader을 구성할 때 shuffle을 한 것이 원인이었다.
test_dataloader = DataLoader(
dataset=test_dataset,
batch_size=batch_size,
shuffle=False, # Note!!! We should NOT shuffle test data
num_workers=num_workers,
pin_memory=True,
prefetch_factor=2,
)
여기서 shuffle=True로 세팅하고, 마지막에 test data ID는 순서대로 뽑았다. 이러면 test data의 예측값과 ID가 매칭이 안 된다. 이러한 이유로 LB의 값이 현저하게 낮게 나온 것이다. shuffle=False로 하니, 차이가 합리적인 수준으로 줄어들었다. 다시는 이런 실수를 하고 싶지 않아서 ㅋㅋ 가장 처음에 적어둔다.
극도의 불균형 데이터
개인적으로 FM 계열의 모델을 설계하고 실험하는 프로젝트를 진행하고 있었는데, 여기서 사용한 데이터의 1의 비율은 25% 정도로, 토스 데이터에 비해서는 준수한 편이었다. 사실, 이 정도의 불균형 데이터를 현업에서 만나본 적도 없었고 제대로된 경진대회 참여는 사실상 이번이 처음이었다.
1의 비율이 1%이기 때문에, 대회 초반에 Raw BCE 손실 함수를 사용하니, 모델이 모든 테스트 데이터를 0으로 예측하는 문제에 직면했다. 이는 Focal 손실 함수, 또는 BCE with pos_weight 손실 함수를 사용하여 해결할 수 있었다.
https://github.com/ds-wook/toss-next-challenge-solution/blob/main/src/utils/loss.py
toss-next-challenge-solution/src/utils/loss.py at main · ds-wook/toss-next-challenge-solution
토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 대회 5등 모델 제출용 레포지토리 - ds-wook/toss-next-challenge-solution
github.com
일단, BCE 손실 함수에서 pos_weight는 Grid Search을 통해 64로 세팅할 때, validation 성능이 가장 좋음을 확인했다. Focal 손실 함수에서 $\alpha$는 1/0의 비율로, $\gamma$는 1.5로 설정했다. $\gamma$는 1이하로 설정하니 학습이 매우 불안정한 형태를 보여서 1보다는 큰 값으로 설정했다.
추가적으로, Mini-Batch Gradient Descent을 할 때, 하나의 Mini-Batch에서 전체 데이터의 1의 비율인 1%가 유지할 수 있도록 Pytorch Stratified Sampler을 사용했다.
https://github.com/ds-wook/toss-next-challenge-solution/blob/main/src/data/dataset.py#L255-L333
toss-next-challenge-solution/src/data/dataset.py at main · ds-wook/toss-next-challenge-solution
토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 대회 5등 모델 제출용 레포지토리 - ds-wook/toss-next-challenge-solution
github.com
이 샘플러에 대한 ablation study을 진행하지는 않았다. 그러나 랜덤 샘플링을 통해서 Mini-Batch을 구성할 때, 1의 샘플이 몰리거나, 아니면 아예 존재하지 않는 경우를 방지하기 위해서 선택할 수 있는 안전 장치라고 느껴졌다. 전체 데이터에서 1의 비율이 1%이기 때문에 각 Mini-Batch에서 1의 비율이 1%로 조정된다. 1의 샘플의 충분한 절대 수를 확보하기 위해 batch_size을 메모리가 허용하는 내에서 최대로 늘렸다. 물론, 이 또한 ablation study을 진행하지는 않았다. 그러나, 이론적으로 Batch Gradient Descent가 Mini-Batch Gradient Descent에 비해서 학습이 느리지만 안정적으로 수렴함을 생각해보면 batch_size을 늘리는 것 또한 극도의 불균형 데이터에서 안정적인 학습을 위한 유의미한 시도라고 생각한다.
One-Hot Encoding 대신 Label Encoding
태생이 통계학도이다 보니, 범주형 변수는 One-Hot Encoding으로 처리해야한다는 강박증이 있다. 개인 FM 모델링 프로젝트를 할 때도 One-Hot Encoding으로 처리하다가 OOM 에러가 떠서 sparse matrix로 처리했다. sparse matrix로 처리할 때의 단점은 코드가 매우 복잡해진다는 것이다. 메모리를 절약하는 대신, sparse matrix에서 정의한 약속대로 계산해야 한다. 이 대회에서도 sparse matrix로 처음에 범주형 데이터를 처리했는데 이 코드를 살펴보니.. 그냥 마음에 안 들었다 ㅋㅋ 가독성이 떨어지는 것은 당연하다. 거기에 더해서, 확장성도 부족하다. 앞으로 많은 모델을 붙일 것인데 sparse matrix 형태로 가능할까? 더 코드가 복잡해질 것 같은데? 라는 생각이 들었다.
그런데 또 다른 신인 Claude는 똑똑한 해결책을 제시했다 (물론 필자만 모르는 것일 수도). 범주형 변수를 Label Encoding하고 offset을 활용해서 계산을 하라는 것이다.
def _setup_categorical_features(self):
"""Setup categorical feature embeddings and offsets"""
# Create offset mapping for vectorized embedding lookup
# Each value in offset indicates the starting index of that feature's embeddings,
# which means number of unique categories for just previous feature
self.field_offsets = [0]
total_vocab_size = 0
for dim in self.categorical_field_dims:
total_vocab_size += dim
self.field_offsets.append(total_vocab_size)
# Single large embedding table for categorical weights
self.categorical_linear = nn.Embedding(total_vocab_size, 1)
# Register field_offsets as buffer
self.register_buffer(
"field_offsets_tensor",
torch.tensor(self.field_offsets[:-1], dtype=torch.long),
)
https://github.com/ds-wook/toss-next-challenge-solution/blob/main/src/models/fm/base.py#L77-L96
toss-next-challenge-solution/src/models/fm/base.py at main · ds-wook/toss-next-challenge-solution
토스 NEXT ML CHALLENGE : 광고 클릭 예측(CTR) 대회 5등 모델 제출용 레포지토리 - ds-wook/toss-next-challenge-solution
github.com
범주형 변수의 범주를 정수형으로 인코딩하고 각 범주형 변수의 cardinality을 offset으로 더한다. 정수형의 범주에 offset을 더하면 해당 범주의 임베딩을 뽑을 수 있다. 이 방법을 보고 무릎을 탁! 쳤다. 코드도 매우 간결해질 뿐만 아니라 확장성까지 챙길 수 있는 전략이었다. 당장 이 방법으로 범주형 변수를 처리하고 이 형태를 대회 끝까지 쭉 유지했다.
NaN Gradient or Loss
정확하게 기억은 나지 않는데, 학습 중간에 Gradient 또는 Loss가 NaN으로 나와서 전체적인 학습이 엉망이 되는 경우가 있었다. 아마도 Loss가 NaN이기 때문에 Gradient도 NaN이 나오는 것이 아닐까 싶었다. 학습 중간에 NaN 값이 발생하고 이를 Gradient Descent 해버리면 weight 값이 NaN으로 될 위험이 있다. 따라서 training main script에 NaN detection 확인 로직을 추가했다.
Importance of Feature Scaling
스케일링을 하기 전에는, train / val loss가 다소 진동하면서 수렴하는 형태를 보였다. elbow의 형태를 보이긴 하는데 튀는 값들이 눈에 거슬렸다. 물론 loss의 스케일 자체가 작기 때문에, 0.001 정도의 차이만 발생해도 line plot 상으로는 튀는 것처럼 보일 수 있으나 그래도 여전히 마음에 들지 않았다.
캐글 엑스퍼트 분께서 경험적으로 GaussRank 스케일링을 했을 때, 딥러닝 모델의 성능이 안정적이었다고 조언을 주셔서 적용을 해봤다. 역시나 경험은 자산이다. 진동하며 수렴하는 loss curve의 형태가 안정적으로 수렴하는 형태로 바뀜을 확인했다. 다양한 종류의 스케일러가 있을 것 같은데 필자는 이 대회에서 GaussRank 스케일러를 사용했다. 다만, 다른 스케일러에 대한 ablation study는 진행하지 않아서 스케일러 간의 비교는 생략한다.
Learning Rate의 중요성
Learning Rate은 Gradient Descent을 할 때, 파라미터를 학습하는 정도를 조절한다. 이 값이 크다면 파라미터가 큰 폭으로 업데이트 된다. 따라서 global optima에 도달하기가 힘들 수 있다. 이 값이 작다면 파라미터가 작은 폭으로 업데이트 된다. 따라서 local minima에 stuck 될 수 있다. 필자는 대회 초기에 learning rate을 특별한 튜닝 없이 0.001로 설정했다. 그러다가 fm, deepfm의 성능이 잘 나오지 않아서 0.0001로 설정했더니 성능이 올라가는 것 아닌가! 이때, 모델 구조에 대해서 고민할 것이 아니라 기본적인 하이퍼 파라미터를 잘 세팅하는 것이 매우 중요함을 깨달았다. 0.00001로도 실험을 해봤는데, 학습이 매우 느리게 진행되었기 때문에 0.0001이 적당하다고 생각했다.
사실, 학습 초기에는 공격적으로 학습을 하다가 후기에는 작은 폭으로 학습할 필요가 있다. 이러한 니즈에 딱 맞게 나온 것이 Learning Rate Scheduling 이다. 필자는 0.0001에서 학습을 시작하여 0.00001에서 끝나도록 스케줄링을 설정했다. 이 세팅에서, 학습 초기에는 고정된 learning rate 세팅과 큰 차이점을 보이지 않았으나 학습 후기에 세밀한 업데이트를 통해 약간의 메트릭 개선이 됨을 실험적으로 확인했다.
FM 모델의 Second Order Interaction Term의 문제
가장 기본적인 형태의 FM 모델을 설계했을 때, logits의 값이 극단적으로 매우 작거나 큰 수로 계산 됨을 확인했다. FM 모델의 second order interaction term에서 제곱항을 포함하기 때문에 발생하는 현상으로 분석했다. 이를 방지하기 위해 clipping을 했고 안정적인 logit 값을 얻을 수 있었다.
second_order = torch.clamp(second_order, -100, 100)
...
total_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(), max_norm=1.0
)
사실, 이렇게 임의로 clipping을 하는게 찜찜하긴 했다. 그러나, BCE with pos_weight과 같은 손실 함수에서 1에 대한 weight을 크게 주기 때문에 발생할 수 있는 현상이고, 따라서 안정적인 학습을 할 수 있는 다른 선택지는 찾지 못하여 대회 끝까지 쭉 적용했다.
FM $\approx$ DeepFM ?
fm 모델을 실험하고 곧 바로 deepfm으로 넘어갔다. 그런데, loss 값, 메트릭 등에서 deepfm이 fm과 거의 동일한 모습을 보이는 것이다. deepfm은 fm component + dnn component으로 구성된다. 따라서, fm component의 스케일이 크거나 모델이 여기서 대부분의 학습을 해버리면 dnn component은 쓸모 없게 된다. 한 마디로, fm component에서 패턴을 찾아버려서 dnn component에서는 학습할 요소가 없는 상황으로 해석했다.
필자는 deepfm이 dnn component가 있으므로 fm이 못하는 비선형 모델링을 할 수 있을 것이라 기대하며 실험을 했는데 뚜껑을 열어보니까 deepfm도 fm component에서 대부분의 학습을 진행하고 있는 것이다. 실제로 디버깅을 통해 fm component, dnn component을 살펴보니 dnn component의 스케일이 현저히 작음을 확인했다. 이를 해결하기 위해 fm component, dnn component에 서로 다른 learning rate을 적용했다. dnn component의 learning rate이 5~10배 크게 설정하는 것이다. 이 방법을 사용하니, fm component와 dnn component간의 스케일이 어느 정도 맞고 학습 양상도 달라지는 것을 확인했다. 다만, 절대적인 성능이 좋지 않아서 deepfm은 빠르게 접었다.
Which Interaction Works Best Here?
처음에는 로지스틱 회귀부터 시작해서 전통 FM, DeepFM 모델을 설계했다. 그러나 부스팅에 비해서 한참 못 미치는 성능이 나오는 것이다. 앙상블도 좋은 애들끼리 하는 것이지.. 너무 뒤떨어지는 모델을 앙상블하는 것은 앙상블 철학에 맞지 않다고 생각한다. 따라서, 단일 모델의 성능을 끌어올리는 것이 가장 중요한데.. DeepFM까지 실험했을 때는 전혀 만족스럽지 않은 성능을 얻었다.
생각해보면 FM, DeepFM은 기본적으로 변수 임베딩간의 Dot Product을 기반으로 interaction을 모델링한다. 필자는interaction을 모델링할 때, 내적이 과연 가장 좋은 선택지인가 라는 의문이 들었다. 실 데이터는 꽤나 복잡한 관계를 가지고 있을텐데, 내적 말고 다른 선택지는 없을까? 라는 생각이 들어서 그때부터 다른 방향으로 interaction을 모델링하는 CTR 모델을 찾아봤다. 이때 찾은 모델들이 xdeepfm, fibinet, ffm, dcn, dcn_v2 이다.
정말 다행히도 dcn 계열의 모델이 준수한 validation 메트릭을 보임을 확인했다. deepfm으로는 0.35을 못넘겼는데 dcn 계열의 모델을 사용하니 0.353 후반까지 찍을 수 있었다 (이 대회에서 0.001은 매우 큰 점수이다 ㅋㅋ). 물론 이 점수도 부스팅에 비해서는 한참 낮은 수준이지만, 그래도 앙상블할 수준까지 끌어 올렸다는 점에서 고무적인 결과라고 생각한다.
사실 시간이 없어서, 그래서 왜 dcn의 interaction이 이 대회에서 잘 먹혔는가? 라는 질문에 대한 해답은 찾지 못했다. 대신, 여러 interaction 형태를 실험하여 풀어냈다. 추후에 FM 모델링 개인 프로젝트를 할 때는, interaction 형태 간의 미묘한 차이를 정밀하게 분석하여 모델 선택할 때 의사결정의 기준으로 삼는 것이 중요할 것이라는 생각이 들었다.
하나 첨언하면, 이렇게 다양한 모델을 빠르게 실험할 수 있었던 원동력은 의심의 여지 없이 Git이다. Git은 신이다 ㅋㅋ. Git으로 기본 파이프라인 뼈대를 잘 잡았기 때문에 모델을 추가하는 것은 매우 쉬운 일이었다. 앞으로도 대회에 참여할 때는 Git을 사용할 것 같다.
Sequence 변수를 어떻게 통합할 것인가?
우리 팀은 이 대회의 핵심 Kick을 Sequence 변수라고 생각했다. 유저가 광고를 클릭하기까지의 서비스 내에서 여정. 이 변수에서 '순서'의 의미를 부스팅 모델에서 살리기는 쉽지 않았다. 따라서 모델을 유연하게 확장할 수 있는 딥러닝 모델에서 '순서'의 의미를 담은 Sequencce 변수를 모델에 통합하고자 했다.
대회의 Baseline은 LSTM을 사용했다. 필자는 LSTM보다 Multi-Head Attention이 다음의 이유 때문에 더 나은 선택지라고 생각했다. 1) LSTM은 병렬 연산이 불가능하기 때문에 학습하는데 시간이 너무 오래 걸린다. 2) BERT에서 Bi-directional Attention을 사용한 것처럼 Causal Masking이 없는 MHA을 붙여서 더 나은 성능을 기대할 수 있다.
MHA을 붙이기 때문에 d_model, nhead, FFM 등의 튜닝 포인트가 존재했다. Grid Search을 통해 GPU 메모리가 허용하는 한도 내에서 적당한 학습 시간을 보이는 d_model = 128, nhead = 8로 세팅하고 FFM은 붙이지 않았다.
사실 Sequence을 여러 방법을 통해 임베딩할 수 있다. 이 변수만 따로 분리해서 BERT의 목적 함수를 사용하여 학습하거나, 아니면 설계한 dcn 모델에 붙여서 BCE loss을 공유할 수도 있다. 필자는 아무래도 Sequence 임베딩이 유저의 클릭 여부를 반영하여 학습되는 것이 더 낫다고 생각하여 Sequence 임베딩을 interaction 임베딩에 concat하여 모델을 설계했다. 사실 이 부분이 아쉽긴하다. Sequence 변수를 따로 분리하여 '순서'의 의미만을 학습하는 것이 더 나은 선택지인지 시간 및 자원 문제로 확인하지 못했기 때문이다. 흥미로운 점은, 대회가 끝나고 dcn, dcn_v2 모델에 MHA을 붙이는 것에 대한 ablation study로 각 경우를 제출했는데 MHA을 붙인 모델의 LB가 더 떨어지는 것이다! 아마도 dcn + MHA로 학습한게 최적의 모델 설계는 아니지 않았을까.. 라는 생각을 해본다.
부족한 점
로지스틱 회귀부터 시작할 필요가 있었을까?
필자는 밑바닥부터 실험하는 것을 좋아해서 가장 기본이 되는 분류 모형인 로지스틱 회귀부터 설계했다. 물론, 로지스틱 회귀 모형을 코딩하면서 파이프라인의 기본 틀을 만들었고 공통으로 사용할 다수의 함수도 구상을 했다. 그런데, 빠르게 좋은 성능을 내는 것이 목표인 경진대회에서 굳이 로지스틱 회귀부터 설계할 필요가 있었을까 라는 의문이 들긴 한다. 나름 빠르게 이 모델을 버리고 다음 모델로 가긴 했는데 처음부터 CTR SoTA 모델로 가는 것이 시간을 절약하는 방법이 아니었을까 라는 생각이 문득 든다.
하이퍼 파라미터 튜닝, 특히 딥러닝 모델에서
딥러닝 모델은 튜닝할 하이퍼 파라미터가 많다. batch_size, learning_rate, regularization, embedding dimension, pos weight in BCE 등... 이 파라미터는 공통적으로 튜닝해야할 것들이다. 모델별로 튜닝해야할 파라미터가 또 따로 있다. 사실 이 대회에서 파라미터를 튜닝할 때, Grid Search에 의존했다. optuna 등 더 고차원적인 튜닝 방법이 존재하지만 시간상 이것까지 적용할 수 없었다.
사실, 딥러닝 모델 학습 하나만 하는데 적어도 5시간이 걸리는데, 파라미터 space을 탐색하며 튜닝을 어떻게 해야하지? 라는 의문과 궁금증이 있다. 그리고, 파라미터 튜닝 라이브러리에 대해서 제대로 알지 못하고 사용해본 적이 없기 때문에 이번 대회에서도 외면했다고 생각한다. 대회도 끝났고 캐글 대회에 관심이 생겼으니.. 각을 잡고 많이 사용하는 하이퍼 파라미터 튜닝 라이브러리와 딥러닝에서 튜닝은 어떻게 하는지 조사해야 겠다.
빠른 베이스라인 모델 빌드
대회에 참여하면서 어느정도 준수한 성능을 내는 모델을 빠르게 빌드하는 것이 중요함을 느꼈다. 캐슬 마스터 분께서 부스팅 모델을 사용하여 준수한 모델 성능을 확보하셔서 이 모델을 기준으로 삼을 수 있었다. 필자는 fm 계열의 모델을 파면서 0.353점 대의 점수를 거의 대회가 시작하고 2~3주 뒤에 확보할 수 있었다. 캐글 엑스퍼트 분은 이미 이 점수대는 대회 초기에 달성하셨다.
우리 팀은 부스팅 모델이 있었기에 망정이지, 만약에 필자 혼자서 대회에 참여했었다면 중간에 대회를 포기했을 것 같았다. 준수한 성능의 베이스라인 모델의 중요성을 다시 한번 느꼈다.
딥러닝 모델의 부족한 성능
아무리 딥러닝 모델을 개선해도, 부스팅 모델을 따라갈 수가 없었다. 딥러닝 모델은 GPU을 사용하여 최소 5시간 정도 학습하기 때문에 효율 측면에서도 부스팅 모델이 딥러닝 모델을 압도했다. 부스팅 모델이 왜 우수한 성능을 보이는지 정확한 원인을 파악할 수는 없다. 부스팅과 dcn 모델은 구조부터가 다르기 때문에 1-1 비교가 불가능하기 때문이다. 다만, 경진대회에서 사람들이 왜 부스팅 모델을 많이 쓰는지 그 이유를 알 수 있었다. 빠르게 좋은 성능을 확보할 수 있기 때문이다.
새롭게 알게된 점
Boosting is All You Need
위에서 언급한 것처럼, 부스팅은 정말 강력한 모델임을 피부로 느꼈다. 학습도 오래 걸리지 않고 준수한 성능을 빠르게 확보할 수 있기 때문이다. 더불어, 경진대회에 참여하려면 부스팅 모델은 그냥 기본 소양이라는 생각이 들었다. 하지만, 부스팅이라고 해서 다 좋은 성능을 내는 것은 아닐 것이다. 부스팅도 LightGBM, XGBoost, CatBoost의 종류가 있고 각 모델의 특징이 다르다. 하이퍼 파라미터는 당연히 다양하며 어떤 학습 방법 (Dart, GOSS)을 선택하느냐에 따라서 학습 결과가 극명하게 갈리기도 한다. 이런 측면에서 당연한 말이지만 부스팅 모델을 '그냥 아는 것'이 아니라 '잘 아는 것'이 중요할 것이다.
Ensemble is All You Need
앙상블은 정말 신기한 기술이다. 모델끼리 잘 섞으면 가장 좋은 성능의 단일 모델보다 더 좋은 성능을 거둘 수 있기 때문이다. 예를 들어서, 부스팅 모델의 성능이 0.3502라고 하자. dcn 모델의 성능은 0.346이다. 필자가 생각할 때, 이 모델을 섞으면 0.3502와 0.346의 어느 중간의 성능을 보이지 않을까 라고 생각했는데 오히려 0.351이라는 성능이 나온다. 이게 앙상블의 매력이다. 물론, 캐글 엑스퍼트 분께서 시그모이드 앙상블이라는 기술을 사용하셨기 때문에 더 좋은 성능이 나온 측면도 있을 것이다. 또한, 이게 더 재미있는게 부스팅 모델로만 앙상블을 하니, 오히려 성능이 떨어졌다. 우리 팀의 초기 가설, 부스팅과 딥러닝 모델을 섞으면 더 좋은 성능이 나온다, 이 맞아 떨어지는 순간이었다. 부스팅과 dcn은 서로 다른 구조를 가지는 모델이다. 즉, 다양한 모델을 앙상블에 섞으면 결과가 좋아지는 것이다! 0.00001 점이 소중한 경진대회에서 이런 앙상블 기술은 필수 중에 필수라고 느껴졌다.
Trust Your CV Score !
Public 점수는 테스트 데이터의 30%, Private 점수는 테스트 데이터의 70%이다. 최종 순위는 Private 점수 기준으로 산출된다. 하지만, 대회하는 내내 참여자들이 볼 수 있는 것은 Public 점수 뿐이다. 다만, 참여자들이 나은 방향으로 모델링을 하고 있는지 확인하는 방법이 있다. CV와 Public 점수의 상관 관계를 따져보는 것이다. train 데이터에서 계산한 CV가 올라갈 수록 제출한 모델의 Public 점수가 올라간다면, 올바른 방향으로 모델링하고 있음을 의미한다 (캐글 엑스퍼트 분께서 알려주셨다 ㅎㅎ) 물론, Public 점수가 올라감에도 보이지 않은 Private 점수는 오히려 하락할 수도 있다. 그러나, 참여자들이 알 수 없는 영역이기 때문에 어떻게 할 수가 없지 않은가? 코드에 leakage가 없다는 가정 하에, CV와 Public 점수가 양의 상관 관계를 보이면, 내가 하고 있는 실험의 CV 점수를 믿어도 된다. 사실, 이게 경진대회 참여자가 할 수 있는 최선이다.
0.0001 증가가 주는 기쁨
대회에 참여하면서 소수점 다섯 번째, 여섯 번째 자리의 값에 따라서 순위가 바뀌는 것을 실시간으로 지켜봤다. 사실, 아주 작은 점수 차이에 의해서 순위가 바뀌면 스트레스가 적지 않았다. 이런 점수 차이가 의미가 있나? 라는 생각도 많이 들었다 ㅋㅋ 현업에서는 서빙 문제로 CV 모델을 당연히 사용하지 못하고 앙상블은 꿈도 못 꾸기 때문이다. 하지만, 어디까지나 환경이 다른 것이다. 현재 필자는 경진대회에 참여하고 있고 이 환경에서는 0.0001점이 소중하다. 사실, 이런 환경이기 때문에 어디 실수한 것이 없나, leakage는 없나 코드를 더욱 꼼꼼하게 보게 된다. 그리고 조금이라도 점수를 올리기 위해 많은 시도를 해본다. 이런 try 자체가 ML을 하는 사람으로서 실력을 더 키우는 경험이 아닐까 라는 생각을 하게 된다. 실제로 캐글 엑스퍼트 분도 캐글 경험을 통해서 피쳐 엔지니어링을 더 잘하게 되었다고 말씀하셨다. 경진대회라는 환경이 현업 환경과 괴리가 있는 것은 사실이지만, 이 환경에서 최선을 다하면 그것이 결국 본인의 실력으로도 이어질 수 있음을 알 수 있었다.
총평
ML 분야에 뛰어들며 메트릭 기반 경진대회를 이렇게 제대로 참여한 적은 처음이었다. 최근 현업에서 느낄 수 없는 도파민을 제대로 느껴서 빡셌지만 아주 기억에 남은 강렬한 한달이었다. 매우 다양한 실험을 하며 느꼈던 점, 부족하다고 생각한 점도 많이 존재한다. 이런 부족한 점을 보완하여 캐글 엑스퍼트에 도전하고 싶은 욕심이 생겼다 ㅋㅋ 물론 정말 쉽지 않은 길이라고 생각한다. 지난 한달도 매우 빡셌는데 캐글은 대회 기간이 세달 (ㄷㄷ)이기 때문이다. 그러나 ML을 하는 사람으로써 도전해볼법한 분야가 아닐까 라는 생각이 들었고 장기적인 목표로 설정하고 싶어졌다.
끝으로 한달 동안 도파민 터지는 경험을 선사해주신 팀원 분들께 진심으로 감사의 마음을 전한다. 남은 2025년은 쩝쩝으로 함께하길 ^^
댓글