- scripts/tune_hyperparams.py: Optuna + Walk-Forward 5폴드 AUC 목적 함수 - 데이터셋 1회 캐싱으로 모든 trial 공유 (속도 최적화) - num_leaves <= 2^max_depth - 1 제약 강제 (소규모 데이터 과적합 방지) - MedianPruner로 저성능 trial 조기 종료 - 결과: 콘솔 리포트 + models/tune_results_YYYYMMDD_HHMMSS.json - requirements.txt: optuna>=3.6.0 추가 - README.md: 하이퍼파라미터 자동 튜닝 사용법 섹션 추가 - docs/plans/: 설계 문서 및 구현 플랜 추가 Made-with: Cursor
185 lines
6.6 KiB
Markdown
185 lines
6.6 KiB
Markdown
# Optuna 하이퍼파라미터 자동 튜닝 설계 문서
|
|
|
|
**작성일:** 2026-03-02
|
|
**목표:** 봇 운영 로그/학습 결과를 바탕으로 LightGBM 하이퍼파라미터를 Optuna로 자동 탐색하고, 사람이 결과를 확인·승인한 후 재학습에 반영하는 수동 트리거 파이프라인 구축
|
|
|
|
---
|
|
|
|
## 배경 및 동기
|
|
|
|
현재 `train_model.py`의 LightGBM 파라미터는 하드코딩되어 있다. 봇 성능이 저하되거나 데이터가 축적될 때마다 사람이 직접 파라미터를 조정해야 한다. 이를 Optuna로 자동화하되, 과적합 위험을 방지하기 위해 **사람이 결과를 먼저 확인하고 승인하는 구조**를 유지한다.
|
|
|
|
---
|
|
|
|
## 구현 범위 (2단계)
|
|
|
|
### 1단계 (현재): LightGBM 하이퍼파라미터 튜닝
|
|
- `scripts/tune_hyperparams.py` 신규 생성
|
|
- Optuna + Walk-Forward AUC 목적 함수
|
|
- 결과를 JSON + 콘솔 리포트로 출력
|
|
|
|
### 2단계 (추후): 기술 지표 파라미터 확장
|
|
- RSI 임계값, MACD 가중치, Stochastic RSI 임계값, 거래량 배수, 진입 점수 임계값 등을 탐색 공간에 추가
|
|
- `dataset_builder.py`의 `_calc_signals()` 파라미터화 필요
|
|
|
|
---
|
|
|
|
## 아키텍처
|
|
|
|
```
|
|
scripts/tune_hyperparams.py
|
|
├── load_dataset() ← 데이터 로드 + 벡터화 데이터셋 1회 생성 (캐싱)
|
|
├── objective(trial, dataset) ← Optuna trial 함수
|
|
│ ├── trial.suggest_*() ← 하이퍼파라미터 샘플링
|
|
│ ├── num_leaves 상한 강제 ← 2^max_depth - 1 제약
|
|
│ └── _walk_forward_cv() ← Walk-Forward 교차검증 → 평균 AUC 반환
|
|
├── run_study() ← Optuna study 실행 (TPESampler + MedianPruner)
|
|
├── print_report() ← 콘솔 리포트 출력
|
|
└── save_results() ← JSON 저장 (models/tune_results_YYYYMMDD_HHMMSS.json)
|
|
```
|
|
|
|
---
|
|
|
|
## 탐색 공간 (소규모 데이터셋 보수적 설계)
|
|
|
|
| 파라미터 | 범위 | 타입 | 근거 |
|
|
|---|---|---|---|
|
|
| `n_estimators` | 100 ~ 600 | int | 데이터 적을 때 500+ 트리는 과적합 |
|
|
| `learning_rate` | 0.01 ~ 0.2 | float (log) | 낮을수록 일반화 유리 |
|
|
| `max_depth` | 2 ~ 7 | int | 트리 깊이 상한 강제 |
|
|
| `num_leaves` | 7 ~ min(31, 2^max_depth-1) | int | **핵심**: leaf-wise 과적합 방지 |
|
|
| `min_child_samples` | 10 ~ 50 | int | 리프당 최소 샘플 수 |
|
|
| `subsample` | 0.5 ~ 1.0 | float | 행 샘플링 |
|
|
| `colsample_bytree` | 0.5 ~ 1.0 | float | 열 샘플링 |
|
|
| `reg_alpha` | 1e-4 ~ 1.0 | float (log) | L1 정규화 |
|
|
| `reg_lambda` | 1e-4 ~ 1.0 | float (log) | L2 정규화 |
|
|
| `time_weight_decay` | 0.5 ~ 4.0 | float | 시간 가중치 강도 |
|
|
|
|
### 핵심 제약: `num_leaves <= 2^max_depth - 1`
|
|
|
|
LightGBM은 leaf-wise 성장 전략을 사용하므로, `num_leaves`가 `2^max_depth - 1`을 초과하면 `max_depth` 제약이 무의미해진다. trial 내에서 `max_depth`를 먼저 샘플링한 후 `num_leaves` 상한을 동적으로 계산하여 강제한다.
|
|
|
|
```python
|
|
max_depth = trial.suggest_int("max_depth", 2, 7)
|
|
max_leaves = min(31, 2 ** max_depth - 1)
|
|
num_leaves = trial.suggest_int("num_leaves", 7, max_leaves)
|
|
```
|
|
|
|
---
|
|
|
|
## 목적 함수: Walk-Forward AUC
|
|
|
|
기존 `train_model.py`의 `walk_forward_auc()` 로직을 재활용한다. 데이터셋은 study 시작 전 1회만 생성하여 모든 trial이 공유한다 (속도 최적화).
|
|
|
|
```
|
|
전체 데이터셋 (N개 샘플)
|
|
├── 폴드 1: 학습[0:60%] → 검증[60%:68%]
|
|
├── 폴드 2: 학습[0:68%] → 검증[68%:76%]
|
|
├── 폴드 3: 학습[0:76%] → 검증[76%:84%]
|
|
├── 폴드 4: 학습[0:84%] → 검증[84%:92%]
|
|
└── 폴드 5: 학습[0:92%] → 검증[92%:100%]
|
|
목적 함수 = 5폴드 평균 AUC (최대화)
|
|
```
|
|
|
|
### Pruning (조기 종료)
|
|
|
|
`MedianPruner` 적용: 각 폴드 완료 후 중간 AUC를 Optuna에 보고. 이전 trial들의 중앙값보다 낮으면 나머지 폴드를 건너뛰고 trial 종료. 전체 탐색 시간 ~40% 단축 효과.
|
|
|
|
---
|
|
|
|
## 출력 형식
|
|
|
|
### 콘솔 리포트
|
|
|
|
```
|
|
============================================================
|
|
Optuna 튜닝 완료 | 50 trials | 소요: 28분 42초
|
|
============================================================
|
|
Best AUC : 0.6234 (Trial #31)
|
|
Baseline : 0.5891 (현재 train_model.py 고정값)
|
|
개선폭 : +0.0343 (+5.8%)
|
|
------------------------------------------------------------
|
|
Best Parameters:
|
|
n_estimators : 320
|
|
learning_rate : 0.0412
|
|
max_depth : 4
|
|
num_leaves : 15
|
|
min_child_samples : 28
|
|
subsample : 0.72
|
|
colsample_bytree : 0.81
|
|
reg_alpha : 0.0023
|
|
reg_lambda : 0.0891
|
|
time_weight_decay : 2.31
|
|
------------------------------------------------------------
|
|
Walk-Forward 폴드별 AUC:
|
|
폴드 1: 0.6102
|
|
폴드 2: 0.6341
|
|
폴드 3: 0.6198
|
|
폴드 4: 0.6287
|
|
폴드 5: 0.6241
|
|
평균: 0.6234 ± 0.0082
|
|
------------------------------------------------------------
|
|
결과 저장: models/tune_results_20260302_143022.json
|
|
다음 단계: python scripts/train_model.py --tuned-params models/tune_results_20260302_143022.json
|
|
============================================================
|
|
```
|
|
|
|
### JSON 저장 (`models/tune_results_YYYYMMDD_HHMMSS.json`)
|
|
|
|
```json
|
|
{
|
|
"timestamp": "2026-03-02T14:30:22",
|
|
"n_trials": 50,
|
|
"elapsed_sec": 1722,
|
|
"baseline_auc": 0.5891,
|
|
"best_trial": {
|
|
"number": 31,
|
|
"auc": 0.6234,
|
|
"fold_aucs": [0.6102, 0.6341, 0.6198, 0.6287, 0.6241],
|
|
"params": { ... }
|
|
},
|
|
"all_trials": [ ... ]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 사용법
|
|
|
|
```bash
|
|
# 기본 실행 (50 trials, 5폴드)
|
|
python scripts/tune_hyperparams.py
|
|
|
|
# 빠른 테스트 (10 trials, 3폴드)
|
|
python scripts/tune_hyperparams.py --trials 10 --folds 3
|
|
|
|
# 데이터 경로 지정
|
|
python scripts/tune_hyperparams.py --data data/combined_15m.parquet --trials 100
|
|
```
|
|
|
|
---
|
|
|
|
## 파일 변경 목록
|
|
|
|
| 파일 | 변경 | 설명 |
|
|
|---|---|---|
|
|
| `scripts/tune_hyperparams.py` | **신규 생성** | Optuna 튜닝 스크립트 |
|
|
| `requirements.txt` | **수정** | `optuna` 의존성 추가 |
|
|
| `README.md` | **수정** | 튜닝 사용법 섹션 추가 |
|
|
|
|
---
|
|
|
|
## 향후 확장 (2단계)
|
|
|
|
`dataset_builder.py`의 `_calc_signals()` 함수를 파라미터화하여 기술 지표 임계값도 탐색 공간에 추가:
|
|
|
|
```python
|
|
# 추가될 탐색 공간 예시
|
|
rsi_long_threshold = trial.suggest_int("rsi_long", 25, 40)
|
|
rsi_short_threshold = trial.suggest_int("rsi_short", 60, 75)
|
|
vol_surge_mult = trial.suggest_float("vol_surge_mult", 1.2, 2.5)
|
|
entry_threshold = trial.suggest_int("entry_threshold", 3, 5)
|
|
stoch_low = trial.suggest_int("stoch_low", 10, 30)
|
|
stoch_high = trial.suggest_int("stoch_high", 70, 90)
|
|
```
|