comsol_surrogate_model / src /mlp_kfold.py
guminhong's picture
Upload 3 files
d1ebc0e verified
Raw
History Blame Contribute Delete
21.4 kB
"""
mlp_kfold.py β€” MLP 단일 λͺ¨λΈ + K-Fold ꡐ차검증
================================================
단일 λΆ„ν• μ˜ 운(運)을 μ œκ±°ν•˜κ³  전체 데이터에 λŒ€ν•΄
평균±std둜 μ‹ λ’°μ„± μžˆλŠ” μ„±λŠ₯을 ν‰κ°€ν•œλ‹€.
섀계:
- μΈ΅ν™” K-Fold (k_decay Γ— p_ve κΈ°μ€€)둜 데이터λ₯Ό Kλ“±λΆ„
- 각 foldκ°€ ν•œ λ²ˆμ”© testκ°€ 됨 (전체 μΌ€μ΄μŠ€κ°€ μ •ν™•νžˆ 1λ²ˆμ”© 평가됨)
- λ‚˜λ¨Έμ§€μ—μ„œ val 일뢀 뢄리, λ‚˜λ¨Έμ§€ train
- K개 λͺ¨λΈμ˜ test μ§€ν‘œλ₯Ό 평균±std둜 보고
- 각 fold λͺ¨λΈ/μŠ€μΌ€μΌλŸ¬ μ €μž₯
λͺ¨λΈ μ„€μ • (κΈ°μ‘΄ 졜적):
ꡬ쑰 [128,256,256,128] SiLU, dropout=0, l2=1e-5
μ‹œκ°„κ°€μ€‘ MSE(tau=2000), epochs=1000, patience_es=80
μ‹€ν–‰: python mlp_kfold.py
"""
from __future__ import annotations
import os
os.environ["PYTHONHASHSEED"] = "42"
os.environ["TF_DETERMINISTIC_OPS"] = "1"
os.environ["TF_CUDNN_DETERMINISTIC"] = "1"
import json
import random
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
def set_korean_font():
candidates = ["AppleGothic", "Malgun Gothic", "NanumGothic",
"NanumBarunGothic", "DejaVu Sans"]
available = {f.name for f in fm.fontManager.ttflist}
for font in candidates:
if font in available:
plt.rcParams["font.family"] = font
break
plt.rcParams["axes.unicode_minus"] = False # μœ λ‹ˆμ½”λ“œ λ§ˆμ΄λ„ˆμŠ€ λŒ€μ‹  ASCII ν•˜μ΄ν”ˆ μ‚¬μš©
set_korean_font()
# 폰트 글리프 κ²½κ³  μˆ¨κΉ€ (U+2212 λ“± λ¬΄ν•΄ν•œ μΉ˜ν™˜ κ²½κ³ )
import logging
logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
import warnings
warnings.filterwarnings("ignore", message="Glyph.*missing from font")
warnings.filterwarnings("ignore", message=".*does not have a glyph.*")
from sklearn.metrics import mean_absolute_error, r2_score
import tensorflow as tf
tf.get_logger().setLevel("ERROR") # retracing λ“± λ¬΄ν•΄ν•œ κ²½κ³  μˆ¨κΉ€
import keras
from keras import layers, callbacks
# ─────────────────────────────────────────────
# 0. μ„€μ •
# ─────────────────────────────────────────────
BASE_DIR = Path(__file__).resolve().parent
DATA_PATH = BASE_DIR / "μ •μ œ_전체_long.xlsx"
OUTPUT_DIR = BASE_DIR / "outputs_kfold"
for d in (OUTPUT_DIR, OUTPUT_DIR / "folds"):
d.mkdir(parents=True, exist_ok=True)
SEED = 42
N_OUTPUTS = 4
N_PARAMS = 10
PARAM_NAMES = ["Lp_ve", "K", "p_le", "sigma_ve", "sigma_le",
"p_ve", "D_gel", "k_decay", "kf_m", "kr_m"]
OUTPUT_NAMES = ["c_lymph", "c_vessel", "c_decay", "c_ecm"]
LOG_TRANSFORM_PARAMS = {"K", "p_le", "p_ve", "D_gel", "k_decay", "kr_m"}
EXCLUDE_CASES = {304, 312, 313, 317}
# === K-Fold μ„€μ • ===
N_FOLDS = 5 # 5-fold (각 fold test β‰ˆ 20%)
VAL_RATIO = 0.15 # train λ‚΄μ—μ„œ val둜 λ–ΌλŠ” λΉ„μœ¨
STRATIFY_BINS = 5 # μΈ΅ν™” κΈ°μ€€ λΆ„μœ„μˆ˜
# === μ™Έλ”΄ μΌ€μ΄μŠ€ μ œμ™Έ μ„€μ • ===
# 곑선이 νŒŒλΌλ―Έν„° 이웃과 크게 λ‹€λ₯Έ(=데이터가 성겨 보간 λΆˆκ°€λŠ₯ν•œ) μΌ€μ΄μŠ€λ₯Ό
# ν•™μŠ΅/평가 전에 μ œμ™Έ. λͺ¨λΈ κ²°κ³Όκ°€ μ•„λ‹Œ 데이터 ꡬ쑰만으둜 νŒμ •ν•˜λ―€λ‘œ 객관적.
# λͺ©μ : 데이터 컀버리지가 μΆ©λΆ„ν•œ 정상 μ˜μ—­μ—μ„œμ˜ 본래 μ„±λŠ₯ μΈ‘μ •.
# μ œμ™Έλœ μ˜μ—­μ€ μΆ”κ°€ COMSOL μƒ˜ν”Œλ§μœΌλ‘œ 별도 보완.
EXCLUDE_SPARSE = True # True: μ™Έλ”΄ μΌ€μ΄μŠ€ μ œμ™Έ, False: 전체 μ‚¬μš©
SPARSE_NEIGHBORS = 5 # κ΅­μ†Œ 뢈일치 κ³„μ‚°μš© 이웃 수
SPARSE_IQR_K = 1.5 # μ΄μƒμΉ˜ μž„κ³„ (Q3 + KΓ—IQR)
# === λͺ¨λΈ μ„€μ • (κΈ°μ‘΄ 졜적) ===
HIDDEN_DIMS = [128, 256, 256, 128]
DROPOUT = 0.0
L2 = 1e-5
TIME_WEIGHT_TAU = 2000.0
EPOCHS = 1000
BATCH_SIZE = 16
LEARNING_RATE = 1e-3
WEIGHT_DECAY = 1e-5
PATIENCE_ES = 80
PATIENCE_LR = 30
MIN_DELTA = 1e-5 # 이보닀 μž‘μ€ val_loss κ°œμ„ μ€ λ¬΄μ‹œ (μ‘°κΈ°μ’…λ£Œ 정상 μž‘λ™)
def set_seed(seed=SEED):
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)
try:
tf.config.experimental.enable_op_determinism()
except Exception:
pass
# ─────────────────────────────────────────────
# 1. 데이터 λ‘œλ“œ
# ─────────────────────────────────────────────
def load_data(path):
df = pd.read_excel(path)
required = {"case", "time_min", *PARAM_NAMES, *OUTPUT_NAMES}
missing = required - set(df.columns)
if missing:
raise ValueError(f"λˆ„λ½λœ 컬럼: {missing}")
if EXCLUDE_CASES:
before = df["case"].nunique()
df = df[~df["case"].isin(EXCLUDE_CASES)].copy()
print(f" μ œμ™Έλœ λ°œμ‚° μΌ€μ΄μŠ€: {sorted(EXCLUDE_CASES)} ({before}->{df['case'].nunique()})")
cases = sorted(df["case"].unique())
n = len(cases)
ref_t = df[df["case"] == cases[0]].sort_values("time_min")["time_min"].values
n_t = len(ref_t)
P = np.zeros((n, N_PARAMS), dtype=np.float64)
C = np.zeros((n, n_t, N_OUTPUTS), dtype=np.float64)
for i, c in enumerate(cases):
sub = df[df["case"] == c].sort_values("time_min")
if len(sub) != n_t or not np.allclose(sub["time_min"].values, ref_t):
raise ValueError(f"case {c}: μ‹œκ°„ κ·Έλ¦¬λ“œ 뢈일치")
P[i] = sub[PARAM_NAMES].iloc[0].values
C[i] = sub[OUTPUT_NAMES].values
n_clip = (C < 0).sum()
if n_clip > 0:
print(f" 음수 농도 클리핑: {n_clip}개 -> 0 (μ΅œμ†Œ {C.min():.4f})")
C = np.clip(C, 0.0, None)
print(f" λ‘œλ“œ μ™„λ£Œ: μΌ€μ΄μŠ€ {n}개, μ‹œκ°„μ  {n_t}개")
return P, C, ref_t, cases
# ─────────────────────────────────────────────
# 2. μŠ€μΌ€μΌλŸ¬
# ─────────────────────────────────────────────
# ─────────────────────────────────────────────
# 1-b. μ™Έλ”΄(sparse) μΌ€μ΄μŠ€ 탐지
# ─────────────────────────────────────────────
def detect_sparse_cases(params, curves):
"""
각 μΌ€μ΄μŠ€μ˜ 곑선이 'νŒŒλΌλ―Έν„° 곡간 μ΄μ›ƒλ“€μ˜ 곑선'κ³Ό μ–Όλ§ˆλ‚˜ λ‹€λ₯Έμ§€(κ΅­μ†Œ 뢈일치)
μΈ‘μ •ν•΄, IQR μ΄μƒμΉ˜λ₯Ό μ™Έλ”΄ μΌ€μ΄μŠ€λ‘œ νŒμ •ν•œλ‹€.
κ΅­μ†Œ λΆˆμΌμΉ˜κ°€ 크닀 = νŒŒλΌλ―Έν„°λŠ” κ°€κΉŒμš΄λ° 거동이 κΈ‰λ³€ = 데이터 해상도 λΆ€μ‘± μ˜μ—­.
λ°˜ν™˜: keep_mask (True=μœ μ§€), sparse_idx (μ œμ™Έ 인덱슀), mismatch (점수)
"""
from scipy.spatial.distance import cdist
log_mask = np.array([n in LOG_TRANSFORM_PARAMS for n in PARAM_NAMES], dtype=bool)
Pl = params.copy().astype(float)
Pl[:, log_mask] = np.log10(np.clip(Pl[:, log_mask], 1e-300, None))
Psc = (Pl - Pl.mean(0)) / (Pl.std(0) + 1e-12)
D = cdist(Psc, Psc)
np.fill_diagonal(D, np.inf)
mismatch = np.zeros(len(params))
for i in range(len(params)):
nn = np.argsort(D[i])[:SPARSE_NEIGHBORS]
neighbor_mean = curves[nn].mean(axis=0)
mismatch[i] = np.abs(curves[i] - neighbor_mean).mean()
q1, q3 = np.percentile(mismatch, [25, 75])
thr = q3 + SPARSE_IQR_K * (q3 - q1)
sparse_idx = np.where(mismatch > thr)[0]
keep_mask = mismatch <= thr
return keep_mask, sparse_idx, mismatch, thr
class ParamScaler:
def __init__(self):
self.log_mask_ = np.array([n in LOG_TRANSFORM_PARAMS for n in PARAM_NAMES], dtype=bool)
self.mean_ = None; self.std_ = None
def _apply_log(self, X):
Xt = X.copy().astype(np.float64)
Xt[:, self.log_mask_] = np.log10(np.clip(Xt[:, self.log_mask_], 1e-300, None))
return Xt
def fit(self, X):
Xt = self._apply_log(X)
self.mean_ = Xt.mean(0); self.std_ = Xt.std(0) + 1e-12
return self
def transform(self, X): return (self._apply_log(X) - self.mean_) / self.std_
def fit_transform(self, X): return self.fit(X).transform(X)
class OutputScaler:
def __init__(self):
self.mean_ = None; self.std_ = None
def fit(self, Y):
Yf = Y.reshape(-1, N_OUTPUTS)
self.mean_ = Yf.mean(0); self.std_ = Yf.std(0) + 1e-12
return self
def transform(self, Y): return (Y - self.mean_) / self.std_
def inverse_transform(self, Y): return Y * self.std_ + self.mean_
def fit_transform(self, Y): return self.fit(Y).transform(Y)
# ─────────────────────────────────────────────
# 3. μΈ΅ν™” K-Fold λΆ„ν• 
# ─────────────────────────────────────────────
def make_strata(params):
"""k_decay Γ— p_ve κΈ°μ€€ μΈ΅ 라벨 생성."""
kd = np.log10(np.clip(params[:, PARAM_NAMES.index("k_decay")], 1e-300, None))
pv = np.log10(np.clip(params[:, PARAM_NAMES.index("p_ve")], 1e-300, None))
kb = pd.qcut(kd, q=STRATIFY_BINS, labels=False, duplicates="drop")
pb = pd.qcut(pv, q=STRATIFY_BINS, labels=False, duplicates="drop")
return kb * (STRATIFY_BINS + 1) + pb
def stratified_kfold_indices(params, n_folds, seed):
"""
μΈ΅ν™” K-Fold: 각 μΈ΅ λ‚΄λΆ€λ₯Ό n_folds둜 λ‚˜λˆ  foldλ§ˆλ‹€ κ³ λ₯΄κ²Œ λΆ„λ°°.
λ°˜ν™˜: fold_assign (각 μΌ€μ΄μŠ€μ˜ fold 번호 0..n_folds-1)
"""
rng = np.random.default_rng(seed)
strata = make_strata(params)
fold_assign = np.full(len(params), -1, dtype=int)
for s in np.unique(strata):
idx = np.where(strata == s)[0]
rng.shuffle(idx)
# μΈ΅ λ‚΄λΆ€λ₯Ό μˆœν™˜ λ°°μ • (각 fold에 κ³ λ₯΄κ²Œ)
for j, i in enumerate(idx):
fold_assign[i] = j % n_folds
return fold_assign
def split_train_val(train_idx, params, val_ratio, seed):
"""train μΈλ±μŠ€μ—μ„œ val을 μΈ΅ν™”λ‘œ 뢄리."""
rng = np.random.default_rng(seed)
strata = make_strata(params)[train_idx]
tr, va = [], []
for s in np.unique(strata):
local = np.where(strata == s)[0]
rng.shuffle(local)
nv = max(1, round(len(local) * val_ratio))
if len(local) - nv < 1:
tr.extend(train_idx[local].tolist()); continue
va.extend(train_idx[local[:nv]].tolist())
tr.extend(train_idx[local[nv:]].tolist())
return np.array(tr), np.array(va)
# ─────────────────────────────────────────────
# 4. 손싀 / λͺ¨λΈ
# ─────────────────────────────────────────────
def make_time_weighted_loss(time_arr, n_t):
w = np.exp(-time_arr / TIME_WEIGHT_TAU)
w = (w / w.mean()).astype(np.float32)
tw = tf.constant(w.reshape(1, n_t, 1), dtype=tf.float32)
@tf.function
def loss_fn(y_true, y_pred):
yt = tf.reshape(y_true, (-1, n_t, N_OUTPUTS))
yp = tf.reshape(y_pred, (-1, n_t, N_OUTPUTS))
return tf.reduce_mean(tf.square(yt - yp) * tw)
return loss_fn
def build_mlp(n_t):
inp = keras.Input(shape=(N_PARAMS,), name="params")
x = inp
for h in HIDDEN_DIMS:
x = layers.Dense(h, kernel_regularizer=keras.regularizers.l2(L2) if L2 > 0 else None)(x)
x = layers.Activation(tf.nn.silu)(x)
if DROPOUT > 0:
x = layers.Dropout(DROPOUT)(x)
out = layers.Dense(n_t * N_OUTPUTS, activation="linear", name="curve")(x)
return keras.Model(inp, out, name="MLP_single")
# ─────────────────────────────────────────────
# 5. 평가 μ§€ν‘œ
# ─────────────────────────────────────────────
def evaluate(y_true, y_pred):
case_mae = np.abs(y_true - y_pred).mean(axis=(1, 2))
per = {name: float(mean_absolute_error(y_true[..., k].reshape(-1),
y_pred[..., k].reshape(-1)))
for k, name in enumerate(OUTPUT_NAMES)}
return {
"MAE_overall": float(mean_absolute_error(y_true.reshape(-1), y_pred.reshape(-1))),
"R2_overall": float(r2_score(y_true.reshape(-1), y_pred.reshape(-1))),
"case_MAE_mean": float(case_mae.mean()),
"case_MAE_max": float(case_mae.max()),
"case_MAE_p90": float(np.percentile(case_mae, 90)),
"case_MAE_std": float(case_mae.std()),
"per_compartment_MAE": per,
}
# ─────────────────────────────────────────────
# 6. 단일 fold ν•™μŠ΅
# ─────────────────────────────────────────────
def train_one_fold(fold, itr, iva, ite, params, curves, time_arr, n_t):
print(f"\n{'='*55}")
print(f" Fold {fold+1}/{N_FOLDS} (train={len(itr)}, val={len(iva)}, test={len(ite)})")
print(f"{'='*55}")
ps = ParamScaler()
X_tr = ps.fit_transform(params[itr]).astype(np.float32)
X_va = ps.transform(params[iva]).astype(np.float32)
X_te = ps.transform(params[ite]).astype(np.float32)
osc = OutputScaler(); osc.fit(curves[itr])
Y_tr = osc.transform(curves[itr]).reshape(len(itr), -1).astype(np.float32)
Y_va = osc.transform(curves[iva]).reshape(len(iva), -1).astype(np.float32)
Y_te_raw = curves[ite]
loss_fn = make_time_weighted_loss(time_arr, n_t)
set_seed(SEED) # foldλ§ˆλ‹€ 동일 μ΄ˆκΈ°ν™” (λͺ¨λΈ 변동 ν†΅μ œ, λΆ„ν•  효과만 μΈ‘μ •)
model = build_mlp(n_t)
model.compile(optimizer=keras.optimizers.Adam(LEARNING_RATE, weight_decay=WEIGHT_DECAY),
loss=loss_fn, metrics=["mae"])
cbs = [
callbacks.EarlyStopping(monitor="val_loss", patience=PATIENCE_ES,
min_delta=MIN_DELTA, restore_best_weights=True, verbose=0),
callbacks.ReduceLROnPlateau(monitor="val_loss", factor=0.5,
patience=PATIENCE_LR, min_delta=MIN_DELTA,
min_lr=1e-7, verbose=0),
]
h = model.fit(X_tr, Y_tr, validation_data=(X_va, Y_va),
epochs=EPOCHS, batch_size=BATCH_SIZE, callbacks=cbs, verbose=0)
ep = len(h.history["val_loss"])
pred = osc.inverse_transform(model.predict(X_te, verbose=0).reshape(-1, n_t, N_OUTPUTS))
metrics = evaluate(Y_te_raw, pred)
print(f" ep={ep} test MAE 평균={metrics['case_MAE_mean']:.3f}% "
f"μ΅œλŒ€={metrics['case_MAE_max']:.3f}% R2={metrics['R2_overall']:.4f}")
# fold μ‚°μΆœλ¬Ό μ €μž₯ (κ°€μ€‘μΉ˜ + μŠ€μΌ€μΌλŸ¬ β†’ μž¬ν˜„/μΆ”λ‘ μš©)
fdir = OUTPUT_DIR / "folds" / f"fold{fold+1}"
fdir.mkdir(parents=True, exist_ok=True)
model.save_weights(str(fdir / "model.weights.h5"))
np.savez(fdir / "scalers.npz",
param_mean=ps.mean_, param_std=ps.std_, param_log_mask=ps.log_mask_,
out_mean=osc.mean_, out_std=osc.std_, time_arr=time_arr,
test_idx=ite)
return metrics, h, pred, Y_te_raw, ite
# ─────────────────────────────────────────────
# 7. μ’…ν•© μ‹œκ°ν™”
# ─────────────────────────────────────────────
def plot_kfold_summary(fold_metrics, histories, save_path):
fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))
fig.suptitle("K-Fold ꡐ차검증 κ²°κ³Ό", fontsize=14, fontweight="bold", y=1.02)
# μ™Όμͺ½: fold별 평균/μ΅œλŒ€ MAE λ§‰λŒ€
ax = axes[0]
folds = [f"F{i+1}" for i in range(len(fold_metrics))]
means = [m["case_MAE_mean"] for m in fold_metrics]
maxes = [m["case_MAE_max"] for m in fold_metrics]
x = np.arange(len(folds)); width = 0.38
ax.bar(x - width/2, means, width, label="평균 MAE", color="#2E5C8A")
ax.bar(x + width/2, maxes, width, label="μ΅œλŒ€ MAE", color="#C62828")
ax.axhline(np.mean(means), color="#2E5C8A", linestyle="--", linewidth=1,
alpha=0.7, label=f"ν‰κ· μ˜ 평균 {np.mean(means):.3f}%")
ax.set_xticks(x); ax.set_xticklabels(folds)
ax.set_ylabel("MAE (%)", fontsize=11)
ax.set_title("Fold별 test MAE", fontsize=12)
ax.legend(fontsize=9); ax.grid(True, alpha=0.3, axis="y")
# 였λ₯Έμͺ½: fold별 val loss 곑선
ax2 = axes[1]
for i, h in enumerate(histories):
vl = h.history["val_loss"]
ax2.plot(np.arange(1, len(vl)+1), vl, linewidth=1.3, alpha=0.8, label=f"Fold {i+1}")
ax2.set_xlabel("Epoch", fontsize=11); ax2.set_ylabel("Val Loss", fontsize=11)
ax2.set_title("Fold별 검증 손싀", fontsize=12)
ax2.set_yscale("log")
ax2.legend(fontsize=8); ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(save_path, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"\n μš”μ•½ κ·Έλž˜ν”„ μ €μž₯: {save_path}")
# ─────────────────────────────────────────────
# 8. 메인
# ─────────────────────────────────────────────
def main():
print("=" * 60)
print(f" MLP {N_FOLDS}-Fold ꡐ차검증")
print("=" * 60)
print("\n[1] 데이터 λ‘œλ“œ")
params, curves, time_arr, cases = load_data(DATA_PATH)
n_t = len(time_arr)
if EXCLUDE_SPARSE:
print("\n[1-b] μ™Έλ”΄ μΌ€μ΄μŠ€ μ œμ™Έ (κ΅­μ†Œ 뢈일치 IQR κΈ°μ€€)")
keep, sparse_idx, mismatch, thr = detect_sparse_cases(params, curves)
excl_cases = [cases[i] for i in sparse_idx]
print(f" μž„κ³„κ°’(Q3+{SPARSE_IQR_K}Γ—IQR) = {thr:.2f}%")
print(f" μ œμ™Έ {len(sparse_idx)}개 / μœ μ§€ {keep.sum()}개")
print(f" μ œμ™Έ μΌ€μ΄μŠ€: {sorted(excl_cases)}")
params = params[keep]
curves = curves[keep]
cases = [c for i, c in enumerate(cases) if keep[i]]
print(f"\n[2] μΈ΅ν™” {N_FOLDS}-Fold λΆ„ν• ")
fold_assign = stratified_kfold_indices(params, N_FOLDS, SEED)
for f in range(N_FOLDS):
print(f" Fold {f+1}: test {int((fold_assign==f).sum())}개")
print("\n[3] Fold별 ν•™μŠ΅")
fold_metrics, histories = [], []
# 전체 μΌ€μ΄μŠ€μ— λŒ€ν•œ out-of-fold 예츑 (각 μΌ€μ΄μŠ€κ°€ testμ˜€μ„ λ•Œμ˜ 예츑)
oof_pred = np.zeros_like(curves)
oof_filled = np.zeros(len(cases), dtype=bool)
for f in range(N_FOLDS):
ite = np.where(fold_assign == f)[0]
rest = np.where(fold_assign != f)[0]
itr, iva = split_train_val(rest, params, VAL_RATIO, SEED + f)
metrics, h, pred, _, test_idx = train_one_fold(
f, itr, iva, ite, params, curves, time_arr, n_t)
fold_metrics.append(metrics); histories.append(h)
oof_pred[test_idx] = pred
oof_filled[test_idx] = True
# ── μ’…ν•© ──
print("\n" + "=" * 60)
print(" K-Fold μ’…ν•© κ²°κ³Ό")
print("=" * 60)
keys = ["case_MAE_mean", "case_MAE_max", "case_MAE_p90", "R2_overall"]
labels = {"case_MAE_mean": "평균 MAE", "case_MAE_max": "μ΅œλŒ€ MAE",
"case_MAE_p90": "p90 MAE", "R2_overall": "RΒ²"}
summary = {}
for k in keys:
vals = np.array([m[k] for m in fold_metrics])
summary[k] = {"mean": float(vals.mean()), "std": float(vals.std()),
"min": float(vals.min()), "max": float(vals.max())}
unit = "" if k == "R2_overall" else "%"
print(f" {labels[k]:10s}: {vals.mean():.3f}{unit} Β± {vals.std():.3f} "
f"[{vals.min():.3f}, {vals.max():.3f}]")
# 전체 OOF μ§€ν‘œ (λͺ¨λ“  μΌ€μ΄μŠ€κ°€ μ •ν™•νžˆ 1λ²ˆμ”© test됨 β†’ κ°€μž₯ 신뒰도 높은 단일 수치)
assert oof_filled.all(), "일뢀 μΌ€μ΄μŠ€κ°€ ν‰κ°€λ˜μ§€ μ•ŠμŒ"
oof_metrics = evaluate(curves, oof_pred)
print(f"\n [전체 OOF] 평균 MAE={oof_metrics['case_MAE_mean']:.3f}% "
f"μ΅œλŒ€={oof_metrics['case_MAE_max']:.3f}% "
f"p90={oof_metrics['case_MAE_p90']:.3f}% RΒ²={oof_metrics['R2_overall']:.4f}")
print(" [전체 OOF κ΅¬νšλ³„ MAE]")
for name, v in oof_metrics["per_compartment_MAE"].items():
print(f" {name:<10}: {v:.3f}%")
print("\n[4] μ €μž₯")
with open(OUTPUT_DIR / "kfold_metrics.json", "w", encoding="utf-8") as fp:
json.dump({"per_fold": fold_metrics, "summary": summary,
"oof": oof_metrics}, fp, indent=2, ensure_ascii=False)
np.savez(OUTPUT_DIR / "oof_predictions.npz",
y_true=curves, y_pred=oof_pred, fold_assign=fold_assign, time_arr=time_arr)
plot_kfold_summary(fold_metrics, histories, OUTPUT_DIR / "kfold_summary.png")
print(f" μ €μž₯: {OUTPUT_DIR}/ (kfold_metrics.json, oof_predictions.npz, "
f"kfold_summary.png, folds/)")
print("\nμ™„λ£Œ.")
if __name__ == "__main__":
main()