M4 Architecture — DL + RL 통합 설계
하나의 API로 7가지 알고리즘을 학습·예측하는 구조
이 글은 AI_Finance(ALCHEMETRIC) 프로젝트의 Module 4 전체 아키텍처를 다룬다. 개별 알고리즘(LSTM, Transformer, DQN, PPO)의 원리는 이전 두 편에서 다뤘으므로, 이번에는 "이것들이 어떻게 하나의 시스템으로 통합되는가"에 집중한다. 라우팅 구조, 학습·예측 파이프라인, 모델 직렬화, DB 통합, API 설계까지 — M4의 뼈대를 조감한다.
목차
1. 설계 원칙 — 왜 통합 아키텍처인가
ALCHEMETRIC의 M3(전통 ML)에는 Random Forest, XGBoost, LightGBM 3종이 있었다. M4에서 LSTM, Transformer, DQN, PPO 4종이 추가되면서 총 7가지 알고리즘을 관리해야 한다.
나쁜 설계는 각 알고리즘마다 별도 API, 별도 DB 테이블, 별도 예측 로직을 만드는 것이다. 이렇게 하면 알고리즘이 추가될 때마다 모든 레이어를 수정해야 한다.
우리가 선택한 원칙은 다음과 같다.
| 원칙 | 설명 |
|---|---|
| 단일 진입점 | 학습은 POST /ml/train 하나, 예측은 POST /ml/predict/{code} 하나 |
| 내부 라우팅 | algorithm 파라미터 값에 따라 내부에서 적절한 Trainer/Predictor로 분기 |
| 통합 DB | ml_model, ml_prediction 테이블을 모든 알고리즘이 공유 |
| 통합 시그널 | 어떤 알고리즘이든 최종 출력은 BUY / SELL / HOLD + confidence |
2. 디렉토리 구조
M4의 코드는 기존 app/ml/ 디렉토리 아래에 두 개의 서브 패키지로 추가됐다.
app/ml/ ├── trainer.py # 통합 진입점 (라우팅) ├── predictor.py # 통합 예측 (라우팅) ├── tuner.py # ML Optuna 튜닝 ├── signal_generator.py # BUY/SELL/HOLD 시그널 ├── feature_engineer.py # 75개 피처 생성 ├── ml_config.yaml # 7종 알고리즘 설정 ├── ml_config_loader.py # YAML 파서 + 타입 판별 │ ├── deep_learning/ # Phase 8A │ ├── architectures.py # LSTMClassifier, TransformerClassifier │ ├── dataset.py # SequenceDataset, 시퀀스 생성 │ ├── dl_trainer.py # DL 학습 파이프라인 │ ├── dl_tuner.py # DL Optuna 튜닝 │ └── dl_predictor.py # DL 추론 │ └── reinforcement/ # Phase 8B ├── environment.py # StockTradingEnv (Gymnasium) ├── rl_trainer.py # RL 학습 파이프라인 ├── rl_tuner.py # RL Optuna 튜닝 └── rl_predictor.py # RL 추론
핵심은 trainer.py와 predictor.py가 M3에서 이미 존재하던 파일이라는 점이다. M4는 이 기존 파일에 라우팅 분기만 추가하고, 실제 DL/RL 로직은 서브 패키지에 격리했다.
Update: ModelTrainer, DeepLearningTrainer, RLTrainer는 현재 모두 BaseTrainer(base_trainer.py)를 상속한다. 데이터 로드, 시계열 분할(_split_data), NaN 필터링, DB 헬퍼 메서드 등 공통 로직이 BaseTrainer로 통합되어 코드 중복이 제거되었다.
3. 알고리즘 라우팅 — 하나의 진입점, 7가지 분기
학습 요청이 들어오면 trainer.py의 ModelTrainer.train()이 호출된다. 이 메서드는 ml_config_loader를 통해 알고리즘 타입을 판별하고, 적절한 전문 Trainer로 위임한다.
라우팅 흐름도
ModelTrainer.train(algorithm="lstm") │ ├─ is_reinforcement(algorithm)? ─── type == "reinforcement" │ └─ Yes → RLTrainer.train() # dqn, ppo │ ├─ is_deep_learning(algorithm)? ─── type == "deep_learning" │ └─ Yes → DeepLearningTrainer.train() # lstm, transformer │ └─ else ─── 전통 ML └─ sklearn/xgb/lgb pipeline # random_forest, xgboost, lightgbm
타입 판별의 비밀: ml_config.yaml
각 알고리즘의 type 필드가 라우팅의 핵심이다.
# ml_config.yaml random_forest: type: classification # → sklearn pipeline xgboost: type: classification # → sklearn pipeline lightgbm: type: classification # → sklearn pipeline lstm: type: deep_learning # → DeepLearningTrainer transformer: type: deep_learning # → DeepLearningTrainer dqn: type: reinforcement # → RLTrainer ppo: type: reinforcement # → RLTrainer
ml_config_loader.py는 YAML을 읽어 두 개의 헬퍼 함수를 제공한다.
def is_deep_learning(algorithm: str) -> bool: # config[algorithm]["type"] == "deep_learning" def is_reinforcement(algorithm: str) -> bool: # config[algorithm]["type"] == "reinforcement"
4. 학습 파이프라인 비교 — ML vs DL vs RL
세 패러다임은 학습 방식이 근본적으로 다르지만, 전처리와 후처리는 거의 동일하다. 아래 표에서 공통 단계와 차이를 비교한다.
| 단계 | ML (M3) | DL (Phase 8A) | RL (Phase 8B) |
|---|---|---|---|
| 데이터 로드 | feature_store | feature_store | feature_store + close |
| NaN 필터링 | 50% 이상 제거 | 50% 이상 제거 | 50% 이상 제거 |
| 결측치 대체 | SimpleImputer (median) | SimpleImputer (median) | SimpleImputer (median) |
| 스케일링 | RobustScaler | RobustScaler | RobustScaler (close 제외) |
| 데이터 분할 | train 85% / val 15% | train 85% / val 15% | train 85% / val 15% |
| 입력 변환 | 2D (N, features) | 3D (N, seq_len, features) | Env 상태벡터 |
| 학습 방식 | model.fit(X, y) | 에폭 루프 + Early Stopping | 에피소드 시뮬레이션 |
| 정답 신호 | target_class (레이블) | target_class (레이블) | reward (보상) |
| Optuna | 50 trials (기본) | 20 trials / 30 epochs | 10 trials / 20K steps |
| 평가 지표 | Accuracy, F1, AUC | Accuracy, F1, AUC | Return, Sharpe, MDD, Win Rate |
| 저장 형식 | .joblib | .pt | .zip + _meta.joblib |
5. 예측 파이프라인 — 파일 확장자로 분기
학습 시에는 algorithm 파라미터로 라우팅했다. 그런데 예측 시에는 저장된 모델 파일만 보고 어떤 Predictor를 쓸지 결정해야 한다. 이때 사용하는 것이 파일 확장자다.
ModelPredictor._run_prediction(ml_model) │ ├─ model_path.endswith(".zip") ── stable-baselines3 형식 │ └─ RLPredictor.predict_single() │ ├─ model_path.endswith(".pt") ── PyTorch 형식 │ └─ DeepLearningPredictor.predict_single() │ └─ else ── joblib (sklearn/xgb/lgb) └─ joblib.load() → model.predict_proba()
왜 확장자인가?
예측 시에는 DB의 ml_model 레코드에서 model_path를 읽어오는데, 이 경로의 확장자가 곧 프레임워크를 의미한다. 별도의 타입 컬럼이나 분기 로직 없이도 파일 자체가 메타데이터 역할을 한다.
| 확장자 | 프레임워크 | 알고리즘 | 부속 파일 |
|---|---|---|---|
| .joblib | scikit-learn / XGBoost / LightGBM | RF, XGB, LGBM | 없음 (모델+scaler 함께 직렬화) |
| .pt | PyTorch | LSTM, Transformer | 없음 (state_dict+scaler+imputer 함께) |
| .zip | stable-baselines3 | DQN, PPO | _meta.joblib (scaler, imputer, features) |
6. 모델 직렬화 — .joblib / .pt / .zip
각 프레임워크의 모델 저장 방식이 다르므로, 저장 시 함께 묶어야 하는 부속품(scaler, imputer, feature 목록 등)의 포장 방식도 다르다.
DL 모델 (.pt)
PyTorch의 torch.save()는 Python dict를 통째로 저장할 수 있다. 이를 활용해 모델 가중치와 전처리 객체를 하나의 .pt 파일에 묶는다.
# 저장 (dl_trainer.py) torch.save({ "model_state_dict": model.state_dict(), "algorithm": "lstm", "model_params": {hidden_size, num_layers, ...}, "scaler": RobustScaler, "imputer": SimpleImputer, "feature_columns": ["rsi_14", "macd", ...], "seq_len": 20, }, path) # 로드 (dl_predictor.py) artifact = torch.load(path) model = LSTMClassifier(**artifact["model_params"]) model.load_state_dict(artifact["model_state_dict"]) scaler = artifact["scaler"]
RL 모델 (.zip + _meta.joblib)
stable-baselines3는 자체 model.save()/Model.load()를 사용하며, 결과물은 .zip 파일이다. 그런데 SB3의 .zip에는 scaler나 imputer를 함께 저장할 수 없으므로, 별도의 _meta.joblib 파일로 메타데이터를 관리한다.
# 저장 (rl_trainer.py) model.save("ppo_kospi_v3.zip") # SB3 모델 joblib.dump({ "scaler": RobustScaler, "imputer": SimpleImputer, "feature_columns": [...], "algorithm": "ppo", "transaction_fee": 0.00015, "tax_rate": 0.0020, }, "ppo_kospi_v3_meta.joblib") # 부속 메타데이터 # 로드 (rl_predictor.py) sb3_model = PPO.load("ppo_kospi_v3.zip") meta = joblib.load("ppo_kospi_v3_meta.joblib")
7. DB 스키마 — 모든 모델을 하나의 테이블로
7가지 알고리즘 모두 동일한 3개 테이블을 사용한다.
ml_model — 학습된 모델 레지스트리
CREATE TABLE ml_model ( id INTEGER PRIMARY KEY, model_name VARCHAR(100), -- "lstm_kospi_v3" algorithm VARCHAR(30), -- lstm / dqn / random_forest ... market VARCHAR(10), -- KOSPI / NASDAQ model_path VARCHAR(500), -- .joblib / .pt / .zip hyperparameters VARCHAR(2000), -- JSON -- 분류 지표 (ML/DL용, RL은 NULL) accuracy NUMERIC(6,4), f1_score NUMERIC(6,4), auc_roc NUMERIC(6,4), is_active BOOLEAN, -- 예측에 사용할 모델 플래그 version INTEGER, ... );
RL 모델은 분류 지표(accuracy, f1 등)가 NULL이다. RL 전용 지표(total_return, sharpe_ratio 등)는 ml_training_log.metrics_json에 JSON으로 저장된다.
ml_prediction — 통합 예측 결과
CREATE TABLE ml_prediction ( model_id INTEGER, -- FK → ml_model code VARCHAR(20), -- "005930" prediction_date DATE, target_date DATE, -- 모든 알고리즘 공통 출력 signal VARCHAR(10), -- BUY / SELL / HOLD confidence NUMERIC(6,4), -- 0.0 ~ 1.0 probability_up NUMERIC(6,4), probability_down NUMERIC(6,4), ... );
어떤 알고리즘의 예측이든 동일한 컬럼에 저장되므로, 대시보드나 비교 분석에서 알고리즘 간 직접 비교가 가능하다.
ml_training_log — 학습 이력 추적
CREATE TABLE ml_training_log ( model_id INTEGER, algorithm VARCHAR(30), status VARCHAR(20), -- running / success / failed -- 유연한 JSON 저장 metrics_json VARCHAR(3000), -- {"f1": 0.72} or {"sharpe": 1.8} hyperparameters_json VARCHAR(2000), feature_importance_json VARCHAR(5000), optuna_trials INTEGER, best_trial_value NUMERIC(10,6), error_message VARCHAR(1000), ... );
8. API 설계 — 알고리즘 무관 인터페이스
API 호출자(대시보드, 스케줄러, 외부 클라이언트)는 algorithm 이름만 바꾸면 된다. 엔드포인트 자체는 변하지 않는다.
| 엔드포인트 | 메서드 | 설명 |
|---|---|---|
| /ml/train | POST | 모델 학습 (algorithm 파라미터로 7종 분기) |
| /ml/predict/{code} | POST | 종목 예측 (model_path 확장자로 자동 분기) |
| /ml/models | GET | 학습된 모델 목록 (전 알고리즘 통합) |
| /ml/predictions | GET | 예측 이력 (전 알고리즘 통합) |
학습 요청 예시
# 전통 ML POST /ml/train { "market": "KOSPI", "algorithm": "random_forest" } # 딥러닝 POST /ml/train { "market": "KOSPI", "algorithm": "lstm" } # 강화학습 POST /ml/train { "market": "KOSPI", "algorithm": "ppo" }
세 요청 모두 동일한 엔드포인트, 동일한 요청 구조다. 내부 라우팅이 나머지를 처리한다.
9. Optuna 통합 — 알고리즘별 탐색 공간
모든 알고리즘은 ml_config.yaml에 search_space를 정의하고, 각 Trainer가 Optuna로 최적 조합을 탐색한다. 하지만 튜닝 전략은 패러다임별로 다르다.
| 항목 | ML (M3) | DL (Phase 8A) | RL (Phase 8B) |
|---|---|---|---|
| 기본 trials | 50 | 20 | 10 |
| 축소 학습량 | 전체 데이터 | 30 epochs (vs 100) | 20K steps (vs 100K) |
| 데이터 축소 | 없음 | 없음 | 4 종목 (vs 8) |
| Pruner | MedianPruner | MedianPruner | 없음 |
| 최적화 목표 | F1 score | F1 score | Total Return |
주요 탐색 공간
LSTM seq_len: 10 ~ 40 (step 5) hidden_size: [64, 128, 256] num_layers: 1 ~ 3 dropout: 0.1 ~ 0.5 lr: 1e-4 ~ 1e-2 (log) Transformer d_model: [64, 128, 256] nhead: [4, 8] ← d_model % nhead == 0 필수 num_layers: 2 ~ 6 dim_ff: [128, 256, 512] DQN learning_rate: 5e-5 ~ 1e-3 (log) buffer_size: [50K, 100K, 200K] gamma: 0.95 ~ 0.999 PPO n_steps: [1024, 2048, 4096] clip_range: 0.1 ~ 0.3 ent_coef: 1e-3 ~ 5e-2 (log)
10. 시그널 통합 — BUY / SELL / HOLD
7가지 알고리즘의 원시 출력은 모두 다르다. ML/DL은 확률값, RL은 액션 번호다. 하지만 최종 소비자(대시보드, 매매 시스템)에게 전달되는 출력은 항상 동일한 포맷이어야 한다.
원시 출력 → 통합 시그널 변환
| 패러다임 | 원시 출력 | 변환 방법 | 결과 |
|---|---|---|---|
| ML / DL | probability_up (0~1) | signal_generator.generate_signal() | signal + confidence |
| RL | action (0/1/2) | 직접 매핑 (0=HOLD, 1=BUY, 2=SELL) | signal + confidence |
RL의 Confidence 계산
ML/DL은 softmax 확률이 자연스럽게 confidence가 된다. RL은 다른 접근이 필요하다.
PPO: 정책 분포에서 액션 확률 추출 dist = model.policy.get_distribution(obs) probs = dist.distribution.probs # [P(HOLD), P(BUY), P(SELL)] confidence = max(probs) DQN: Q-값에 softmax 적용 q_values = model.q_net(obs) # [Q(HOLD), Q(BUY), Q(SELL)] probs = softmax(q_values) confidence = max(probs)
통합 출력 포맷
어떤 알고리즘이든 /ml/predict/{code}의 응답은 동일한 구조다.
{
"model_id": 42,
"model_name": "ppo_kospi_v3",
"algorithm": "ppo",
"prediction_date": "2025-05-16",
"target_date": "2025-05-19",
"signal": "BUY",
"confidence": 0.7823,
"probability_up": 0.7823,
"probability_down": 0.2177
}
11. 전체 흐름 요약
학습과 예측의 전체 데이터 흐름을 하나의 그림으로 정리한다.
학습 흐름
POST /ml/train { algorithm: "ppo" }
│
▼
ml_service → ModelTrainer.train()
│
├─ ml_config_loader: is_reinforcement("ppo") = True
│
▼
RLTrainer.train()
│
├─ 1. feature_store → 데이터 로드 (피처 + close)
├─ 2. NaN 필터 → 50% 이상 컬럼 제거
├─ 3. SimpleImputer → median 대체
├─ 4. RobustScaler → 피처 정규화 (close 제외)
├─ 5. Time split → train 85% / val 15%
├─ 6. Optuna → 10 trials, 20K steps, 4종목
├─ 7. DummyVecEnv → StockTradingEnv × 8종목
├─ 8. PPO.learn() → 100K timesteps
├─ 9. Evaluate → Return, Sharpe, MDD, Win Rate
├─ 10. Save → .zip + _meta.joblib
│
▼
DB: ml_model + ml_training_log INSERT
예측 흐름
POST /ml/predict/005930 │ ▼ ml_service → ModelPredictor.predict() │ ├─ DB: ml_model (is_active=True) 조회 ├─ model_path = "ppo_kospi_v3.zip" ├─ endswith(".zip") → RLPredictor │ ▼ RLPredictor.predict_single() │ ├─ 1. PPO.load(".zip") ├─ 2. joblib.load("_meta.joblib") → scaler, imputer ├─ 3. feature_store → 최신 1행 로드 ├─ 4. imputer.transform() → scaler.transform() ├─ 5. obs = [features, cash_ratio=1.0, stock_ratio=0.0] ├─ 6. action = model.predict(obs, deterministic=True) ├─ 7. confidence = max(policy_probs) ├─ 8. signal = {0:HOLD, 1:BUY, 2:SELL}[action] │ ▼ DB: ml_prediction INSERT │ ▼ Response: { signal: "BUY", confidence: 0.78 }
'Project > 🚀 AI Finance 2026' 카테고리의 다른 글
| #26 [모듈4-2] DL Early Stopping & Overfitting (0) | 2026.03.18 |
|---|---|
| #25 [모듈4-1] PyTorch & Deep Learning (0) | 2026.03.18 |
| #23 [모듈3-2] Optuna — 하이퍼파라미터 자동 튜닝의 모든 것 (0) | 2026.03.16 |
| #22 [모듈3-1] scikit-learn, XGBoost, LightGBM — 3대 ML 라이브러리 (0) | 2026.03.13 |
| #21 [모듈3] Machine Learning (0) | 2026.03.13 |