본문 바로가기
Project/🚀 AI Finance 2026

#24 [모듈4] Deep Learning

by 로띠 2026. 3. 18.
Module 4

M4 Architecture — DL + RL 통합 설계

하나의 API로 7가지 알고리즘을 학습·예측하는 구조

M0 Data M2 Feature M3 ML M4 DL + RL Signal

이 글은 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
한 줄 요약: API 호출자는 알고리즘이 sklearn인지, PyTorch인지, stable-baselines3인지 알 필요가 없다. algorithm 이름만 바꾸면 된다.

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.pypredictor.py가 M3에서 이미 존재하던 파일이라는 점이다. M4는 이 기존 파일에 라우팅 분기만 추가하고, 실제 DL/RL 로직은 서브 패키지에 격리했다.

Update: ModelTrainer, DeepLearningTrainer, RLTrainer는 현재 모두 BaseTrainer(base_trainer.py)를 상속한다. 데이터 로드, 시계열 분할(_split_data), NaN 필터링, DB 헬퍼 메서드 등 공통 로직이 BaseTrainer로 통합되어 코드 중복이 제거되었다.

3. 알고리즘 라우팅 — 하나의 진입점, 7가지 분기

학습 요청이 들어오면 trainer.pyModelTrainer.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"
확장 포인트: 새 알고리즘을 추가하려면 (1) ml_config.yaml에 설정 추가, (2) 해당 타입의 Trainer에 분기 추가 — 이 두 단계면 충분하다. API, DB, 시그널 레이어는 수정할 필요가 없다.

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
핵심 차이: ML은 행 단위 독립 예측, DL은 시퀀스(20일) 패턴 학습, RL은 매매 시뮬레이션으로 전략 자체를 학습한다. 하지만 전처리(NaN 처리, 스케일링)와 후처리(시그널 생성, DB 저장)는 세 패러다임 모두 동일하다.

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")
설계 트레이드오프: .pt는 단일 파일로 깔끔하지만 PyTorch 전용이다. .zip+meta는 파일이 2개로 나뉘지만 SB3의 공식 직렬화 방식을 그대로 활용할 수 있다. 각 프레임워크의 관례를 존중하는 것이 유지보수에 유리하다.

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),
    ...
);
설계 포인트: 분류 지표는 ml_model에 정규 컬럼으로, RL/DL 전용 지표는 training_log의 JSON 필드로 저장한다. 이렇게 하면 기존 ML 스키마를 깨지 않으면서 새로운 지표 체계를 수용할 수 있다.

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.yamlsearch_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_serviceModelTrainer.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_serviceModelPredictor.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 }
아키텍처 요약: M4의 핵심은 "다양성의 통합"이다. 서로 다른 3개 프레임워크(sklearn, PyTorch, SB3), 서로 다른 3가지 패러다임(지도학습, 시퀀스 분류, 강화학습)을 하나의 API, 하나의 DB, 하나의 시그널 체계로 묶었다. 새로운 알고리즘이 추가되어도 ml_config.yaml에 설정을 추가하고 해당 타입의 서브 패키지에 코드를 작성하면 나머지 레이어는 자동으로 따라온다.