feat: enhance model training and deployment scripts with time-weighted sampling
- Updated `train_model.py` and `train_mlx_model.py` to include a time weight decay parameter for improved sample weighting during training. - Modified dataset generation to incorporate sample weights based on time decay, enhancing model performance. - Adjusted deployment scripts to support new backend options and improved error handling for model file transfers. - Added new entries to the training log for better tracking of model performance metrics over time. - Included ONNX model export functionality in the MLX filter for compatibility with Linux servers.
This commit is contained in:
@@ -1,65 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# 맥미니에서 학습한 모델을 LXC 컨테이너 볼륨 경로로 전송한다.
|
||||
# 사용법: bash scripts/deploy_model.sh [LXC_HOST] [LXC_MODELS_PATH]
|
||||
# 사용법: bash scripts/deploy_model.sh [lgbm|mlx]
|
||||
#
|
||||
# 예시:
|
||||
# bash scripts/deploy_model.sh 10.1.10.28 /path/to/cointrader/models
|
||||
# bash scripts/deploy_model.sh root@10.1.10.28 /root/cointrader/models
|
||||
# bash scripts/deploy_model.sh # LightGBM (기본값)
|
||||
# bash scripts/deploy_model.sh mlx # MLX 신경망
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LXC_HOST="${1:-root@10.1.10.24}"
|
||||
LXC_MODELS_PATH="${2:-/root/cointrader/models}"
|
||||
LOCAL_MODEL="models/lgbm_filter.pkl"
|
||||
BACKEND="${1:-lgbm}"
|
||||
LXC_HOST="root@10.1.10.24"
|
||||
LXC_MODELS_PATH="/root/cointrader/models"
|
||||
LOCAL_LOG="models/training_log.json"
|
||||
|
||||
if [[ ! -f "$LOCAL_MODEL" ]]; then
|
||||
echo "[오류] 모델 파일 없음: $LOCAL_MODEL"
|
||||
echo "먼저 python scripts/train_model.py 를 실행하세요."
|
||||
exit 1
|
||||
# ── 백엔드별 파일 목록 설정 ──────────────────────────────────────────────────
|
||||
# mlx: ONNX 파일만 전송 (Linux 서버는 onnxruntime으로 추론)
|
||||
# lgbm: pkl 파일 전송
|
||||
RELOAD_CMD="from src.ml_filter import MLFilter; f=MLFilter(); f.reload_model(); print('리로드 완료')"
|
||||
if [ "$BACKEND" = "mlx" ]; then
|
||||
LOCAL_FILES=("models/mlx_filter.weights.onnx")
|
||||
else
|
||||
LOCAL_FILES=("models/lgbm_filter.pkl")
|
||||
fi
|
||||
|
||||
echo "=== 모델 전송 시작 ==="
|
||||
echo " 대상: ${LXC_HOST}:${LXC_MODELS_PATH}"
|
||||
echo " 파일: $LOCAL_MODEL"
|
||||
|
||||
# 기존 모델을 prev로 백업 (원격)
|
||||
ssh "${LXC_HOST}" "
|
||||
if [ -f '${LXC_MODELS_PATH}/lgbm_filter.pkl' ]; then
|
||||
cp '${LXC_MODELS_PATH}/lgbm_filter.pkl' '${LXC_MODELS_PATH}/lgbm_filter_prev.pkl'
|
||||
echo ' 기존 모델 백업 완료'
|
||||
# ── 파일 존재 확인 ────────────────────────────────────────────────────────────
|
||||
for f in "${LOCAL_FILES[@]}"; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo "[오류] 모델 파일 없음: $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== 모델 전송 시작 (백엔드: ${BACKEND}) ==="
|
||||
echo " 대상: ${LXC_HOST}:${LXC_MODELS_PATH}"
|
||||
|
||||
# ── 원격 디렉터리 생성 + lgbm 기존 모델 백업 ─────────────────────────────────
|
||||
ssh "${LXC_HOST}" "
|
||||
mkdir -p '${LXC_MODELS_PATH}'
|
||||
if [ '$BACKEND' = 'lgbm' ] && [ -f '${LXC_MODELS_PATH}/lgbm_filter.pkl' ]; then
|
||||
cp '${LXC_MODELS_PATH}/lgbm_filter.pkl' '${LXC_MODELS_PATH}/lgbm_filter_prev.pkl'
|
||||
echo ' 기존 lgbm 모델 백업 완료'
|
||||
fi
|
||||
"
|
||||
|
||||
# 모델 파일 전송 (rsync 우선, 없으면 scp 폴백)
|
||||
if command -v rsync &>/dev/null && ssh "${LXC_HOST}" "command -v rsync" &>/dev/null; then
|
||||
rsync -avz --progress \
|
||||
"$LOCAL_MODEL" \
|
||||
"${LXC_HOST}:${LXC_MODELS_PATH}/lgbm_filter.pkl"
|
||||
else
|
||||
echo " rsync 없음 → scp 사용"
|
||||
scp "$LOCAL_MODEL" "${LXC_HOST}:${LXC_MODELS_PATH}/lgbm_filter.pkl"
|
||||
fi
|
||||
|
||||
# 학습 로그도 함께 전송 (있을 경우)
|
||||
if [[ -f "$LOCAL_LOG" ]]; then
|
||||
# ── 파일 전송 헬퍼 (rsync 우선, scp 폴백) ────────────────────────────────────
|
||||
_send() {
|
||||
local src="$1" dst="$2"
|
||||
echo " 전송: $src → ${LXC_HOST}:$dst"
|
||||
if command -v rsync &>/dev/null && ssh "${LXC_HOST}" "command -v rsync" &>/dev/null; then
|
||||
rsync -avz "$LOCAL_LOG" "${LXC_HOST}:${LXC_MODELS_PATH}/training_log.json"
|
||||
rsync -avz --progress "$src" "${LXC_HOST}:$dst"
|
||||
else
|
||||
scp "$LOCAL_LOG" "${LXC_HOST}:${LXC_MODELS_PATH}/training_log.json"
|
||||
scp "$src" "${LXC_HOST}:$dst"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 모델 파일 전송 ────────────────────────────────────────────────────────────
|
||||
for f in "${LOCAL_FILES[@]}"; do
|
||||
_send "$f" "${LXC_MODELS_PATH}/$(basename "$f")"
|
||||
done
|
||||
|
||||
# ── 학습 로그 전송 ────────────────────────────────────────────────────────────
|
||||
if [[ -f "$LOCAL_LOG" ]]; then
|
||||
_send "$LOCAL_LOG" "${LXC_MODELS_PATH}/training_log.json"
|
||||
echo " 학습 로그 전송 완료"
|
||||
fi
|
||||
|
||||
echo "=== 전송 완료 ==="
|
||||
echo ""
|
||||
|
||||
# 봇 컨테이너가 실행 중이면 모델 핫리로드, 아니면 건너뜀
|
||||
# ── 핫리로드 ─────────────────────────────────────────────────────────────────
|
||||
echo "=== 핫리로드 시도 ==="
|
||||
if ssh "${LXC_HOST}" "docker inspect -f '{{.State.Running}}' cointrader 2>/dev/null | grep -q true"; then
|
||||
ssh "${LXC_HOST}" "docker exec cointrader python -c \
|
||||
\"from src.ml_filter import MLFilter; f=MLFilter(); f.reload_model(); print('리로드 완료')\""
|
||||
ssh "${LXC_HOST}" "docker exec cointrader python -c \"${RELOAD_CMD}\""
|
||||
echo "=== 핫리로드 완료 ==="
|
||||
else
|
||||
echo " cointrader 컨테이너가 실행 중이 아닙니다. 건너뜁니다."
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# 맥미니에서 전체 학습 파이프라인을 실행하고 LXC로 배포한다.
|
||||
# 사용법: bash scripts/train_and_deploy.sh [LXC_HOST] [LXC_MODELS_PATH]
|
||||
# 사용법: bash scripts/train_and_deploy.sh [mlx|lgbm]
|
||||
#
|
||||
# 예시:
|
||||
# bash scripts/train_and_deploy.sh
|
||||
# bash scripts/train_and_deploy.sh root@10.1.10.24 /root/cointrader/models
|
||||
# bash scripts/train_and_deploy.sh # LightGBM (기본값)
|
||||
# bash scripts/train_and_deploy.sh mlx # MLX GPU 학습
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -19,37 +19,35 @@ else
|
||||
echo "경고: 가상환경을 찾을 수 없습니다 ($VENV_PATH). 시스템 Python을 사용합니다." >&2
|
||||
fi
|
||||
|
||||
LXC_HOST="${1:-root@10.1.10.24}"
|
||||
LXC_MODELS_PATH="${2:-/root/cointrader/models}"
|
||||
BACKEND="${1:-lgbm}"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== [1/3] 데이터 수집 (XRP + BTC + ETH 3심볼) ==="
|
||||
echo "=== [1/3] 데이터 수집 (XRP + BTC + ETH 3심볼, 1년치) ==="
|
||||
python scripts/fetch_history.py \
|
||||
--symbols XRPUSDT BTCUSDT ETHUSDT \
|
||||
--interval 1m \
|
||||
--days 90 \
|
||||
--days 365 \
|
||||
--output data/xrpusdt_1m.parquet
|
||||
# 결과: data/combined_1m.parquet (타임스탬프 기준 병합)
|
||||
|
||||
echo ""
|
||||
echo "=== [2/3] 모델 학습 (21개 피처: XRP 13 + BTC/ETH 상관관계 8) ==="
|
||||
# TRAIN_BACKEND=mlx 로 설정하면 Apple Silicon GPU(Metal)를 사용한다 (기본: lgbm)
|
||||
BACKEND="${TRAIN_BACKEND:-lgbm}"
|
||||
DECAY="${TIME_WEIGHT_DECAY:-2.0}"
|
||||
if [ "$BACKEND" = "mlx" ]; then
|
||||
echo " 백엔드: MLX (Apple Silicon GPU)"
|
||||
python scripts/train_mlx_model.py --data data/combined_1m.parquet
|
||||
echo " 백엔드: MLX (Apple Silicon GPU), decay=${DECAY}"
|
||||
python scripts/train_mlx_model.py --data data/combined_1m.parquet --decay "$DECAY"
|
||||
else
|
||||
echo " 백엔드: LightGBM (CPU)"
|
||||
python scripts/train_model.py --data data/combined_1m.parquet
|
||||
echo " 백엔드: LightGBM (CPU), decay=${DECAY}"
|
||||
python scripts/train_model.py --data data/combined_1m.parquet --decay "$DECAY"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== [3/3] LXC 배포 ==="
|
||||
bash scripts/deploy_model.sh "$LXC_HOST" "$LXC_MODELS_PATH"
|
||||
bash scripts/deploy_model.sh "$BACKEND"
|
||||
|
||||
echo ""
|
||||
echo "=== 전체 파이프라인 완료 ==="
|
||||
echo ""
|
||||
echo "봇 재시작이 필요하면:"
|
||||
echo " ssh ${LXC_HOST} 'cd /root/cointrader && docker compose restart cointrader'"
|
||||
echo " ssh root@10.1.10.24 'cd /root/cointrader && docker compose restart cointrader'"
|
||||
|
||||
@@ -25,14 +25,14 @@ MLX_MODEL_PATH = Path("models/mlx_filter.weights")
|
||||
LOG_PATH = Path("models/training_log.json")
|
||||
|
||||
|
||||
def train_mlx(data_path: str) -> float:
|
||||
def train_mlx(data_path: str, time_weight_decay: float = 2.0) -> float:
|
||||
print(f"데이터 로드: {data_path}")
|
||||
df = pd.read_parquet(data_path)
|
||||
print(f"캔들 수: {len(df)}")
|
||||
|
||||
print("\n데이터셋 생성 중...")
|
||||
t0 = time.perf_counter()
|
||||
dataset = generate_dataset_vectorized(df)
|
||||
dataset = generate_dataset_vectorized(df, time_weight_decay=time_weight_decay)
|
||||
t1 = time.perf_counter()
|
||||
print(f"데이터셋 생성 완료: {t1 - t0:.1f}초, {len(dataset)}개 샘플")
|
||||
|
||||
@@ -46,10 +46,30 @@ def train_mlx(data_path: str) -> float:
|
||||
|
||||
X = dataset[FEATURE_COLS]
|
||||
y = dataset["label"]
|
||||
w = dataset["sample_weight"].values
|
||||
|
||||
split = int(len(X) * 0.8)
|
||||
X_train, X_val = X.iloc[:split], X.iloc[split:]
|
||||
y_train, y_val = y.iloc[:split], y.iloc[split:]
|
||||
w_train = w[:split]
|
||||
|
||||
# --- 클래스 불균형 처리: 언더샘플링 (가중치 인덱스 보존) ---
|
||||
pos_idx = np.where(y_train == 1)[0]
|
||||
neg_idx = np.where(y_train == 0)[0]
|
||||
|
||||
if len(neg_idx) > len(pos_idx):
|
||||
np.random.seed(42)
|
||||
neg_idx = np.random.choice(neg_idx, size=len(pos_idx), replace=False)
|
||||
|
||||
balanced_idx = np.concatenate([pos_idx, neg_idx])
|
||||
np.random.shuffle(balanced_idx)
|
||||
|
||||
X_train = X_train.iloc[balanced_idx]
|
||||
y_train = y_train.iloc[balanced_idx]
|
||||
w_train = w_train[balanced_idx]
|
||||
|
||||
print(f"\n언더샘플링 적용 후 학습 데이터: {len(X_train)}개 (양성={y_train.sum()}, 음성={(y_train==0).sum()})")
|
||||
# --------------------------------------
|
||||
|
||||
print("\nMLX 신경망 학습 시작 (GPU)...")
|
||||
t2 = time.perf_counter()
|
||||
@@ -60,7 +80,7 @@ def train_mlx(data_path: str) -> float:
|
||||
epochs=100,
|
||||
batch_size=256,
|
||||
)
|
||||
model.fit(X_train, y_train)
|
||||
model.fit(X_train, y_train, sample_weight=w_train)
|
||||
t3 = time.perf_counter()
|
||||
print(f"학습 완료: {t3 - t2:.1f}초")
|
||||
|
||||
@@ -83,6 +103,7 @@ def train_mlx(data_path: str) -> float:
|
||||
"auc": round(auc, 4),
|
||||
"samples": len(dataset),
|
||||
"train_sec": round(t3 - t2, 1),
|
||||
"time_weight_decay": time_weight_decay,
|
||||
"model_path": str(MLX_MODEL_PATH),
|
||||
})
|
||||
with open(LOG_PATH, "w") as f:
|
||||
@@ -94,8 +115,12 @@ def train_mlx(data_path: str) -> float:
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data", default="data/xrpusdt_1m.parquet")
|
||||
parser.add_argument(
|
||||
"--decay", type=float, default=2.0,
|
||||
help="시간 가중치 감쇠 강도 (0=균등, 2.0=최신이 ~7.4배 높음)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
train_mlx(args.data)
|
||||
train_mlx(args.data, time_weight_decay=args.decay)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -146,7 +146,7 @@ def generate_dataset(df: pd.DataFrame, n_jobs: int | None = None) -> pd.DataFram
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def train(data_path: str):
|
||||
def train(data_path: str, time_weight_decay: float = 2.0):
|
||||
print(f"데이터 로드: {data_path}")
|
||||
df_raw = pd.read_parquet(data_path)
|
||||
print(f"캔들 수: {len(df_raw)}, 컬럼: {list(df_raw.columns)}")
|
||||
@@ -169,7 +169,7 @@ def train(data_path: str):
|
||||
df = df_raw[base_cols].copy()
|
||||
|
||||
print("데이터셋 생성 중...")
|
||||
dataset = generate_dataset_vectorized(df, btc_df=btc_df, eth_df=eth_df)
|
||||
dataset = generate_dataset_vectorized(df, btc_df=btc_df, eth_df=eth_df, time_weight_decay=time_weight_decay)
|
||||
|
||||
if dataset.empty or "label" not in dataset.columns:
|
||||
raise ValueError(f"데이터셋 생성 실패: 샘플 0개. 위 오류 메시지를 확인하세요.")
|
||||
@@ -183,10 +183,30 @@ def train(data_path: str):
|
||||
print(f"사용 피처: {len(actual_feature_cols)}개 {actual_feature_cols}")
|
||||
X = dataset[actual_feature_cols]
|
||||
y = dataset["label"]
|
||||
w = dataset["sample_weight"].values
|
||||
|
||||
split = int(len(X) * 0.8)
|
||||
X_train, X_val = X.iloc[:split], X.iloc[split:]
|
||||
y_train, y_val = y.iloc[:split], y.iloc[split:]
|
||||
w_train = w[:split]
|
||||
|
||||
# --- 클래스 불균형 처리: 언더샘플링 (가중치 인덱스 보존) ---
|
||||
pos_idx = np.where(y_train == 1)[0]
|
||||
neg_idx = np.where(y_train == 0)[0]
|
||||
|
||||
if len(neg_idx) > len(pos_idx):
|
||||
np.random.seed(42)
|
||||
neg_idx = np.random.choice(neg_idx, size=len(pos_idx), replace=False)
|
||||
|
||||
balanced_idx = np.concatenate([pos_idx, neg_idx])
|
||||
np.random.shuffle(balanced_idx)
|
||||
|
||||
X_train = X_train.iloc[balanced_idx]
|
||||
y_train = y_train.iloc[balanced_idx]
|
||||
w_train = w_train[balanced_idx]
|
||||
|
||||
print(f"\n언더샘플링 적용 후 학습 데이터: {len(X_train)}개 (양성={y_train.sum()}, 음성={(y_train==0).sum()})")
|
||||
# --------------------------------------
|
||||
|
||||
model = lgb.LGBMClassifier(
|
||||
n_estimators=300,
|
||||
@@ -201,6 +221,7 @@ def train(data_path: str):
|
||||
)
|
||||
model.fit(
|
||||
X_train, y_train,
|
||||
sample_weight=w_train,
|
||||
eval_set=[(X_val, y_val)],
|
||||
callbacks=[lgb.early_stopping(30, verbose=False), lgb.log_evaluation(50)],
|
||||
)
|
||||
@@ -225,9 +246,11 @@ def train(data_path: str):
|
||||
log = json.load(f)
|
||||
log.append({
|
||||
"date": datetime.now().isoformat(),
|
||||
"backend": "lgbm",
|
||||
"auc": round(auc, 4),
|
||||
"samples": len(dataset),
|
||||
"features": len(actual_feature_cols),
|
||||
"time_weight_decay": time_weight_decay,
|
||||
"model_path": str(MODEL_PATH),
|
||||
})
|
||||
with open(LOG_PATH, "w") as f:
|
||||
@@ -239,8 +262,12 @@ def train(data_path: str):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data", default="data/xrpusdt_1m.parquet")
|
||||
parser.add_argument(
|
||||
"--decay", type=float, default=2.0,
|
||||
help="시간 가중치 감쇠 강도 (0=균등, 2.0=최신이 ~7.4배 높음)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
train(args.data)
|
||||
train(args.data, time_weight_decay=args.decay)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user