Clear Hour: 하루가 시작되기 전에 공기질 예측을 Git에 커밋하고 공개적으로 점수를 매겼습니다
요약
이 글은 오픈 웨이트 모델 TabPFN v2를 활용하여 특정 지역의 공기질을 24시간 단위로 순위화하는 'Clear Hour' 시스템 개발 과정을 공유합니다. 이 시스템은 예측 데이터를 하루가 시작되기 전에 Git에 커밋하고, 실제 측정값과 비교 점수를 매기는 원칙을 고수합니다. 이를 통해 예보 모델의 객관성과 신뢰성을 높이는 방법을 제시합니다.
핵심 포인트
- TabPFN v2를 이용해 24시간 공기질 순위를 예측함.
- 예측 데이터를 사전에 커밋하여 객관적인 검증 과정을 거침.
- 실제 측정값과 비교 점수를 매겨 모델의 신뢰성을 확보함.
_이 글은 Hacktoberfest 오픈 소스 AI 챌린지 주차 1: 신선한 공기 시간(Touch Grass)_에 제출하는 내용입니다. 🌫️
요약: 클리어 아워(Clear Hour)는 오픈 웨이트 테이블 기반 모델인 TabPFN v2를 사용하여 이슬라마바드/라왈핀디의 내일 24시간 동안 공기가 얼마나 숨 쉬기 좋은지 순위를 매깁니다. 저는 이 모델을 노트북 CPU에서 로컬로 구동하여, 정부가 제공하는 무료 공기질 예보를 1년간의 실제 센서 측정값과 비교 보정합니다. 매일의 예보는 그 날이 시작되기 전에 git에 커밋되며, 이후 실제로 센서가 측정한 값과 점수가 매겨집니다. 제가 오늘 아침 첫 번째 예측을 점수화했습니다. TabPFN을 포함한 세 모델 모두 시간을 역순으로 순위를 매겼습니다. 하지만 저는 이 결과를 공개할 것입니다. 왜냐하면 나중에 보기 좋은 날짜를 고르는 대신, 공적으로 틀릴 수 있다는 것이 미리 예보를 등록하는 전체 이유이기 때문입니다.
제가 만든 것 (What I Built)
"풀밭에 가라(Touch grass)"는 대부분의 장소에서 좋은 조언입니다. 하지만 제가 사는 곳에서는 일 년 중 약 4개월 동안은 별표가 붙습니다.
저는 이슬라마바드에 살고 있습니다. 매년 10월이 되면 스모그가 자리 잡기 시작하고, "산책을 가자"는 것이 더 이상 단순한 결정이 아닙니다. 오전 9시의 공기는 오후 5시의 공기와 전혀 다르며, 목이 아파서 밖에 나가기 전까지는 어느 쪽이 어떤지 아무도 알려주지 않습니다. 이곳의 앱들은 _오늘의 수치_를 보여줍니다. 즉, 하루에 4배나 차이가 나는 날을 단일한 색상 코드로 평준화하여 보여주는 이미 오래된 하나의 숫자에 불과합니다.
클리어 아워(Clear Hour)는 대신 내일의 모든 24시간 동안 순위를 매겨, 가장 깨끗한 시간부터 가장 오염된 시간까지 나열합니다. 이 시스템은 외출할 가치가 있는 두 시간 간격과 피해야 할 시간을 선택해줍니다. 그리고 그 답을 _하루가 시작되기 전에 커밋_합니다.
이 마지막 부분이 바로 핵심 원칙입니다. 어떤 날에 우연히 공기가 좋았다는 이유만으로 예보 모델을 사후적으로 돌려보고, 얼마나 잘 작동했는지 글을 쓰는 것은 매우 쉬운 일일 것입니다. 그래서 제가 첫날 설정한 규칙은 다음과 같습니다: 하루의 예보는 그 날이 시작되기 전에 git에 커밋되며, 저는 그 파일을 다시는 건드리지 않습니다. 하루가 끝난 후, 별도의 스크립트가 실제 측정값을 가져와서, 좋든 나쁘든 점수를 매깁니다.
오늘 아침에 실제로 하루 데이터를 가지고 그 점수 산정 스크립트를 처음으로 돌려봤습니다. 이 프로젝트에 대해 제가 드릴 수 있는 가장 솔직한 이야기부터 시작하죠.
데모 (Demo)
실시간: https://abeera81.github.io/clear-hour/
레포지토리 (Repo): https://github.com/Abeera81/clear-hour
로그인이나 빌드 과정 없이, 정적인 페이지 하나로 구성되어 있습니다. 헤드라인을 지나 스크롤하면 2026년 10월 11일의 사전 등록된 예측 정보가 나타나는데, 여기에 센서가 실제로 측정한 값이 그 위에 그려져 있습니다:
| 모델 | MAE | Bias | Spearman (24h) | 가장 깨끗한 2시간대 선택 | 실제 측정값 |
|---|---|---|---|---|---|
| B0 raw CAMS | 21.9 | −3.5 | −0.512 | 05:00–07:00 | 73.7 |
| ... | |||||
| 아무 모델도 하루 중 가장 깨끗한 시간대를 선택하지 못했습니다. Raw CAMS는 인간에게 가장 중요한 방식으로 최악의 성능을 보였습니다. 즉, 아침 5시에 야외 활동을 하도록 안내하며, 그 시간이 하루 전체에서 두 번째로 오염도가 높은 2시간대인 73.7 µg/m³를 가리켰습니다. TabPFN과 B2는 적어도 하루 평균보다 낮은 시간대를 선택했지만, 둘 다 실제 시간대와는 2시간 차이가 났고 저녁에 얼마나 깨끗해질지 심각하게 과소평가했습니다. |
따라서: 이 프로젝트가 현실과 비교되어 점수 매겨진 단 하루 동안, 이 모델은 시간을 거꾸로 예측했습니다. 이것은 하루에 대한 진실한 진술이며, 아무도 클릭하지 않을 후속 게시물이 아닌, 프로젝트 자체의 포스트에 포함되어 있습니다.
이것이 TabPFN에 대한 최종 판결은 아닙니다. 하루는 하나의 샘플일 뿐입니다. 내일 좋은 날이라 하더라도 모델이 작동한다는 것을 증명하지 못합니다. 아래의 30일 홀드아웃(held-out) 평가는 실제 증거가 존재하는 곳이며, 그곳에서 TabPFN은 자신의 자리를 얻습니다.
구축 과정 (How I Built It)
파이프라인 (The pipeline)
모두 무료이고 모두 공개된 세 가지 재료:
| 구성 요소 | 출처 | 기여 내용 |
|---|---|---|
| 대기질 예측 | Open-Meteo (CAMS 글로벌 모델), CC BY 4.0 | 시간별 PM2.5 + 다른 오염물질 5가지, 최대 7일 전까지 |
| ... | ||
| CAMS는 진지한 대기 모델이지만, 이 특정 지점에서는 예측 가능하게 잘못되었습니다. 약 28,000시간이 넘는 중복 시간 동안 미국 대사관 참고 모니터와 비교했을 때, CAMS는 평균적으로 18.9 µg/m³만큼 과소평가하며, 이 오류는 시간대 및 계절에 따라 강하게 달라집니다. 즉, 10월~12월에는 오전 9시 최고치에서 최대 90 µg/m³까지, 저녁 최고치에서 최대 70 µg/m³까지 놓칩니다. 이러한 구조적이고 반복적인 오류는 테이블형 모델(tabular model)이 학습할 수 있는 정확한 종류의 것입니다. |
세 가지 모델 모두 공개, 숨기는 것 없음 (Three models, all shown, none hidden)
모든 예측 카드에는 세 가지가 나란히 표시됩니다. 성능이 낮은 기준선 모델(weak baseline)이라도 멋진 모델이 이겼다고 해서 조용히 제외되지 않습니다:
- B0 (raw CAMS). 수정 없음. 제어 모델(control)입니다.
- B2 (CAMS + 시간대 편향). 하루의 각 시간에 대해 하나의 오프셋을 적용하며, 11,693개의 실제 센서 판독값에 맞춰 조정되었습니다. 의도적으로 단순화한 기준선(baseline)입니다.
- M1 (TabPFN v2). Prior Labs'의 오픈 웨이트 테이블 기초 모델(tabular foundation model)로, CPU에서 로컬로 구동되며, 최대 10,000개의 최근 센서 시간 단위 CAMS + 날씨 특징값에 맞춰 조정되었습니다. 파인튜닝(fine-tuning)도 없고, 하이퍼파라미터 검색도 없으며, 호스팅된 API도 사용하지 않았습니다:
# src/forecast.py: TabPFN v2, local CPU, no tuning
m = TabPFNRegressor.create_default_for_version(
ModelVersion.V2, device="cpu", fit_mode="fit_with_cache",
...
ignore_pretraining_limits=True가 핵심 플래그입니다: 이 플래그 없이는 TabPFN v2는 약 1,000개의 학습 행(row) 이상을 처리하지 못하며, CPU에서 10,000개 행까지 처리하는 이유 중 하나가 바로 적합(fit) 시간이 몇 초가 아니라 30~90분이 걸린다는 점입니다. 조정된 모델은 디스크에 캐시되어(10,000행 기준 6.3 GB) 반복 실행 시 재조정(refitting) 없이 약 2분 만에 다시 로드됩니다. 이 노트북에서 첫 번째 적합 과정에는 3,351초, 즉 56분이 걸렸습니다.
사전 등록(Pre-registration), 그리고 어색했던 부분 포함
한 시간 동안의 조정 작업은
역사를 다시 쓸 수 없는 점수 산정
# scripts/score_forecast.py: 고정된 예측값을 읽고, 절대 수정하지 않습니다.
fc = json.loads((ROOT / "results" / f"forecast_{day}.json").read_text(encoding="utf-8"))
...
...
점수 산정은 두 개의 새로운 파일을 작성할 뿐, 커밋된 예측값을 절대 수정하지 않습니다. CLI는 반대 방향에서도 동일한 규칙을 강제합니다: --save는 results/forecast_<날짜>.json이 이미 존재할 경우, --force 없이 덮어쓰기를 거부하는데, 이는 해당 파일이 사전 등록(pre-registration)일 수 있기 때문입니다. 나조차도 사후에 숫자를 조작할 수는 없습니다.
코드
저장소: https://github.com/Abeera81/clear-hour (MIT; 데이터와 모델 가중치는 자체 라이선스를 유지합니다)
흥미로운 파일들:
src/forecast.py: 엔진(실시간 피처 가져오기, 세 가지 모델, 윈도우 선택)src/clearhour/:python -m clearhourCLI로, 패키징된 시간 편향 오프셋을 포함하여 기본 경로에서 API 키가 필요 없습니다.eval/evaluate.py: 시간적 분할(temporal split);results/eval_test.json이 존재하는 경우 TEST 월에 두 번 실행되는 것을 물리적으로 거부합니다.eval/learning_curve.py: 6개 폴드 순환 원점(
| 모델 | MAE | RMSE | Bias | Within-day ρ (median) | Top-2h hit % |
|---|---|---|---|---|---|
| B0 raw CAMS | 16.1 | 20.7 | −3.4 | 0.564 | 30% |
| ... | |||||
| TabPFN이 제가 시도해 본 모든 모델 중 가장 낮은 오차를 보였으며, B2 대비 MAE 우위는 확실하게 실재합니다 (차이에 대한 95% CI: −17.0 ~ −6.9 µg/m³). 하지만 이 표본 크기에서는 B2 대비 그 순위 우위가 통계적으로 유의미하지 않습니다 (CI: −0.04 ~ +0.29), 그리고 튜닝되지 않은 HistGBR이 실제로 DEV 월에는 TabPFN을 능가했습니다. 정확히 최고의 2시간 간격을 찾는 것은 모든 모델에서 최대 43%에 머뭅니다. 저는 단 하나의 강조된 승리보다는 이 세 가지 주의사항을 언급하는 편이 낫겠습니다. |
TabPFN은 실제로 얼마나 많은 데이터가 필요할까요?
이것은 제가 가장 알고 싶었던 질문이었고, 그 결과는 제가 빼고 싶은 유혹에 빠질 법한 결과를 가져왔습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기