DACON · 2026

ETRI 휴먼 이해 대회 회고

점수는 기대에 못 미쳤지만 기록은 오래 남았다

· 저장소

2026년 6월 26일 오전, DACON ETRI Human Understanding AI Paper Challenge가 끝났다.

막판에는 거의 분 단위로 판단했다. 제출권은 남아 있고, public leaderboard는 생각만큼 움직이지 않고, 로컬에서는 좋아 보이는 후보가 계속 생겼다. "지금 이걸 제출하는 게 맞나?"를 계속 물으면서 파일 이름, 로그, notebook 기록, 제출 메모를 하나씩 맞췄다.

대회가 끝나고 나니 가장 오래 남은 건 모델 구조가 아니었다. 오히려 검증을 믿을 수 없을 때 무엇을 기준으로 의사결정했는지, 그리고 실패한 제출을 나중에 설명할 수 있게 기록했는지가 더 크게 남았다.

어떤 대회였나

스마트폰과 스마트워치에서 수집된 라이프로그를 바탕으로 사람의 상태를 예측하는 문제였다. 타깃은 Q1-Q3의 주관식 상태와 S1-S4의 수면 관련 지표, 총 7개였다.

처음에는 "라이프로그니까 도메인 신호를 잘 뽑으면 되지 않을까?"라고 생각했다. 앱 사용, 활동량, 수면 패턴, 날씨, 대기질 같은 신호를 잘 엮으면 점수가 내려갈 것 같았다. 그런데 실제로는 그렇게 단순하지 않았다. 어떤 feature는 로컬에서는 그럴듯했지만 public leaderboard에서는 거의 반응하지 않았고, 어떤 후보는 로컬 CV가 좋아졌는데 실제 제출 점수는 나빠졌다.

이때부터 대회는 모델링 문제이면서 동시에 운영 문제가 됐다. 새 feature를 만드는 것만큼이나, 그 feature가 진짜 신호인지 아니면 split에 과적합한 착시인지 구분하는 일이 중요했다.

문제의 어려움은 타깃이 하나의 상태로 묶이지 않는다는 데 있었다. Q1-Q3는 수면의 느낌, 피로, 스트레스처럼 사람이 직접 응답한 주관 지표에 가깝고, S1-S4는 수면 시간이나 수면 중 생체/행동 상태에 가까운 지표였다. 같은 라이프로그라도 앱 사용량이 Q3에는 말이 될 수 있고, HR이나 조도, 활동량은 S4 쪽에 더 말이 될 수 있다. 결국 "좋은 feature"라는 표현은 너무 넓었다. 어떤 타깃에, 어떤 시간대에서, 어떤 subject split에서 살아남는지가 훨씬 중요했다.

그래서 후반에는 feature 의미를 타깃별로 나눠 보려고 했다. Q2/Q3에는 앱 사용과 routine, S3/S4에는 수면 전후의 활동량, HR, light, 대기환경을 붙여봤다. 이 접근 자체는 맞는 방향이었지만, public으로 transfer될 만큼 큰 독립 신호를 찾지는 못했다.

끝까지 기준점이 된 ID342

public 기준으로 마지막까지 가장 좋았던 제출은 ID342였다.

id 설명 local CV public LB
342 ID178/ID221 feature-similarity shrink, alpha 0.50 0.534000 0.5960832519

이 후보는 "강한 새 모델 하나"라기보다, 이미 만든 예측 두 계열을 feature similarity 기준으로 보수적으로 줄여 섞은 결과였다. 화려한 아이디어는 아니었지만 안정적이었다. 새로 만든 후보들이 로컬에서 조금씩 이겨도, public에서는 이 기준점을 넘기 어려웠다.

이 지점이 좀 괴로웠다. 연구하고 feature를 붙이면 붙일수록 로컬 점수는 내려가는데, 정작 public에서는 잘 안 먹혔다. 대회 중반 이후에는 ID342가 일종의 방어선이 됐다. 새 후보는 "좋아 보인다"가 아니라 "ID342보다 정말 제출할 가치가 있나?"를 통과해야 했다.

나중에 public-scored 후보들을 놓고 다시 보니, 단순 local CV 전체 순위는 위험했다. ID287은 local 0.378934까지 내려갔지만 public은 0.6698787318로 무너졌다. ID329도 local 0.516718이었지만 public 0.6017908577로 ID342를 이기지 못했다. 반대로 ID342, ID339, ID338이 들어간 2026년 6월 23일 slate는 local 순서와 public 순서가 같이 [342, 339, 338]로 맞았다. 이때부터 "로컬 점수가 낮은가"보다 "실패한 frontier 후보와 같은 방향으로 움직이는가"를 보는 쪽이 더 중요해졌다.

마지막 날의 제출들

마지막 날에는 Q3 app-name lexicon anti-signal 후보를 만들었다. 앱 이름에서 스트레스와 관련된 신호를 뽑아보자는 아이디어였다. 도메인상 말이 됐고, 로컬 게이트도 나쁘지 않았다. 일부 split에서는 꽤 일관되게 이겼다.

그래서 제출했다. 그리고 전부 졌다.

id local CV public LB ID342 대비 public 변화
346 0.533659 0.5963391483 +0.0002558964
350 0.533699 0.5963352712 +0.0002520193
347 0.533813 0.5961947552 +0.0001115033

셋 중 가장 가까웠던 ID347도 ID342보다 나빴다. 수치상으로는 아주 작은 차이지만, 대회 막판에는 이런 작은 차이가 하루 종일 머릿속을 맴돈다. "조금만 더 보수적으로 갔으면?" "아예 제출하지 않는 게 맞았나?" 같은 생각이 남는다.

그래도 이 제출들이 완전히 무의미하다고 생각하지는 않는다. 이들은 적어도 한 가지를 확인해줬다. 내가 만든 Q3 lexicon 신호는 로컬에서는 유용해 보였지만 public split으로는 충분히 transfer되지 않았다. 아쉽지만, 이것도 대회에서 얻는 정보다.

이때 ID348과 ID349도 생성됐지만 DACON daily submission limit에 걸려 제출되지 않았다. 마지막 30분에는 모델링보다 운영 판단이 더 컸다. 제출 파일 이름, submission log, notebook history stub, 제출 메모가 서로 맞아야 했고, 어떤 파일이 실제 public score를 받은 파일인지 나중에 복원 가능해야 했다. 막판에 만든 CSV가 많아질수록 이 매칭이 더 중요해졌다.

외부 데이터와 API 실험

대회 후반에는 날씨와 대기질도 붙여봤다. ETRI와 대전이라는 도메인 prior가 있었고, 사람의 수면과 피로, 스트레스가 환경 신호와 완전히 무관하다고 보기는 어려웠다. 다만 여기서도 중요한 건 "데이터를 가져왔는가"가 아니라 "대회 기간과 타깃에 맞는 historical feature가 되는가"였다.

KMA ASOS 시간자료는 가장 깔끔했다. 대전 ASOS 133번 지점 기준으로 2024년 6월 1일부터 2024년 11월 22일까지 historical hourly observation을 가져올 수 있었다. 온도, 습도, 강수, 풍속, 기압, 일사 같은 변수를 후보로 볼 수 있었다. 하지만 ID342 gate에서는 기대만큼 강한 신호가 나오지 않았다.

생활기상지수 API는 호출 자체보다 시간 범위가 문제였다. UV 지수와 대기정체지수는 forecast 성격이 강했고, 2024년 historical lookup으로 쓰기 어렵다는 smoke 결과가 나왔다. 그래서 train/test feature로 쓰지 않고 report-only로 닫았다.

AirKorea는 처음 받은 API가 실시간/최근 3개월 중심이라 대회 기간과 맞지 않았다. 이후 2024년 historical archive를 받아 다시 점검했고, 대전 13개 측정소에 대해 competition window 2024-06-01 00:00부터 2024-11-22 23:00까지 4200개 hourly timestamp가 모두 있는 것을 확인했다. PM10, PM2.5, O3, NO2, CO, SO2 같은 feature를 만들 수 있었지만, ID342 gate 결과는 통과 0건이었다. 가장 좋은 row도 projected public 0.596056065 수준으로 ID342 주변에 머물렀고, 목표였던 public 0.55와는 여전히 +0.046056065만큼 멀었다.

이 경험은 꽤 좋은 교훈이었다. 외부 데이터는 "있으면 좋아 보이는 것"과 "검증을 통과하는 독립 신호"가 다르다. 특히 수면/스트레스처럼 subject별 baseline이 강한 문제에서는 지역 단위 환경 신호가 너무 부드럽거나, public split에서 충분히 분리되지 않을 수 있다.

Kaggle과 자동화에서 배운 것

Kaggle CLI 문제도 한동안 발목을 잡았다. 처음에는 인증이나 네트워크, slug 문제처럼 보였지만, 작은 Kaggle dataset/kernel probe는 성공했다. 결국 핵심 원인은 Kaggle 입력 패키지가 낡아 있었다는 점이었다.

기존 softkleenex/dacon-etri-human-base 입력은 2026년 6월 17일 snapshot에 가까웠고, 최신 reports/와 post-ID341 산출물이 없었다. runner는 필요한 경로가 없어서 바로 실패했다. 이후 base dataset 위에 patch dataset을 overlay하는 방식으로 바꿨고, 최신 scripts, reports, submissions, base predictions를 덮어씌우게 했다. no-submit upstream rebuild v3는 remote blueprint gate까지 통과했지만, 새로 promotion할 artifact evidence는 나오지 않았다.

이 과정에서 Kaggle은 "제출 버튼을 대신 누르는 곳"이 아니라 "무거운 실험을 격리해서 검증하는 곳"이어야 한다는 생각이 더 강해졌다. 대회 막판에는 특히 자동화가 빠를수록 위험하다. no-submit run, artifact gate, submission log, decision note가 분리되어 있어야 실험이 운영 사고로 번지지 않는다.

이번 대회에서 제일 많이 배운 것

이번 대회의 핵심 문장은 이것이었다.

좋은 로컬 점수와 좋은 제출 후보는 다르다.

로컬 CV는 필요하다. 없으면 아무것도 못 한다. 하지만 로컬 CV가 좋아졌다는 사실만으로 제출 가치가 생기지는 않았다. 특히 타깃별로 신호가 다르고, public/private split의 분포를 완전히 알 수 없는 상황에서는 더 그랬다.

그래서 후반부에는 점수표보다 로그가 더 중요해졌다. 제출 파일 하나, notebook-history stub 하나, submission log 한 행이 서로 맞아야 했다. 그래야 나중에 "이 public score가 어느 파일에서 나온 거지?"라는 질문에 답할 수 있었다.

대회 막판에는 정신이 꽤 흐려진다. 비슷한 이름의 CSV가 많아지고, 제출 메모는 길어지고, 로컬 점수와 public 점수가 섞이기 쉽다. 이때 기록이 없으면 실험이 아니라 운에 가까워진다. 이번에는 적어도 그 부분을 끝까지 붙잡으려고 했다.

잘했던 것과 아쉬운 것

잘했던 건 제출 후보를 꽤 엄격하게 관리하려고 한 점이다. 로컬 점수와 제출 점수를 분리해서 적었고, 위험한 후보는 따로 표시했다. 덕분에 마지막 날에도 어느 후보가 실제로 제출됐고, 어느 후보가 실패했는지 복구할 수 있었다.

아쉬운 건 public feedback을 설명하는 validation lens를 충분히 빨리 만들지 못한 점이다. 날씨, 대기질, 앱 사용량 같은 외부/도메인 feature를 계속 실험했지만, "왜 public에서는 안 먹히는가"를 더 일찍 분석했어야 했다. feature를 더 많이 만드는 것보다, public에서 통하는 신호와 안 통하는 신호의 차이를 먼저 봤어야 했다.

보안 쪽에서도 배운 게 있다. 자동 제출과 API 연동은 편하지만 credential은 절대 코드에 남기면 안 된다. 이번에는 과거 이력에 남은 토큰을 제거하고 history rewrite까지 했다. 대회 repo도 결국 제품 repo처럼 다뤄야 한다.

그리고 목표였던 public 0.55는 끝까지 증명하지 못했다. ID342의 public 0.5960832519는 이전 0.60 벽을 깬 의미 있는 개선이었지만, 상금권을 노리기에는 아직 멀었다. 마지막 날의 Q3 app lexicon도, AirKorea도, raw sequence smoke도 결국 ID342를 안정적으로 넘지 못했다. 이건 단순히 "운이 나빴다"보다 "public에 맞는 validation lens를 너무 늦게 만들었다"에 가깝다.

다음 대회라면

다음에 비슷한 대회를 한다면 초반부터 세 가지를 더 강하게 가져갈 것 같다.

  1. public feedback을 설명하는 검증 set을 빨리 만든다.
  2. 실험 이름, CSV, 로그, notebook 기록을 처음부터 1:1로 맞춘다.
  3. 새 feature가 로컬에서 이겼을 때보다, 왜 이겼는지 설명될 때 제출한다.
  4. 외부 데이터는 먼저 historical coverage와 license, train/test feature 가능성을 검증한다.
  5. Kaggle remote run은 no-submit artifact gate로 유지하고, 제출과 분리한다.

목표했던 0.55에는 닿지 못했다. 솔직히 아쉽다. 점수표만 보면 실패에 가깝다. 그런데 대회가 끝난 뒤 남은 폴더를 보면, 그래도 다음 대회에서 반복하지 않을 실수들이 꽤 선명하게 남아 있다.

그게 이번 대회의 수확이었다. 점수는 기대보다 높았지만, 기록은 생각보다 오래 남았다.