| """ |
| kfold_mlp_app.py โ ํผํ์ฃผ์ฌ ์ฝ๋ฌผ๋ํ ์์ธก ์ฑ (K-Fold ์์๋ธ) |
| ============================================================== |
| mlp_kfold.py ๋ก ํ์ตํ 5๊ฐ fold ๋ชจ๋ธ์ ๋ชจ๋ ๋ถ๋ฌ์ |
| ์์๋ธ ํ๊ท ์ผ๋ก ์์ธกํ๊ณ , fold ๊ฐ ํธ์ฐจ๋ก ์ ๋ขฐ๊ตฌ๊ฐ์ ํ์ํ๋ค. |
| |
| ์ฌ์ ์ค๋น: python mlp_kfold.py ์คํ ํ outputs_kfold/folds/ ์์ฑ |
| ์คํ: streamlit run kfold_mlp_app.py |
| """ |
|
|
| import matplotlib |
| import matplotlib.pyplot as plt |
| import io |
| import numpy as np |
| import pandas as pd |
| import streamlit as st |
| import tensorflow as tf |
| import keras |
| from keras import layers |
| from pathlib import Path |
|
|
| BASE_DIR = Path(__file__).resolve().parent |
| OUTPUT_DIR = BASE_DIR / "outputs_kfold" |
| FOLDS_DIR = OUTPUT_DIR / "folds" |
|
|
| 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"} |
| N_PARAMS = 10 |
| N_OUTPUTS = 4 |
|
|
| |
| HIDDEN_DIMS = [128, 256, 256, 128] |
| L2 = 1e-5 |
|
|
| PARAM_RANGES = { |
| "Lp_ve": (4e-12, 1.6e-11), |
| "K": (1e-16, 1e-13), |
| "p_le": (1e-9, 1e-7), |
| "sigma_ve": (0.01, 0.99), |
| "sigma_le": (0.01, 0.50), |
| "p_ve": (1e-11, 1e-8), |
| "D_gel": (1e-12, 1e-9), |
| "k_decay": (1e-8, 1e-4), |
| "kf_m": (0.1, 3.0), |
| "kr_m": (2.5, 200.0), |
| } |
| DEFAULT_VALUES = np.array([ |
| 8e-12, 1e-15, 1e-8, 0.9, 0.1, |
| 1e-9, 45e-12, 1.7e-6, 0.48, 4.2, |
| ]) |
| COLORS = ["#2ecc71", "#e74c3c", "#3498db", "#f39c12"] |
| _trapz = np.trapezoid if hasattr(np, "trapezoid") else np.trapz |
|
|
|
|
| def _setup_font(): |
| candidates = ["AppleGothic", "Malgun Gothic", "NanumGothic", "Noto Sans CJK KR"] |
| available = {f.name for f in matplotlib.font_manager.fontManager.ttflist} |
| for name in candidates: |
| if name in available: |
| matplotlib.rc("font", family=name) |
| break |
| matplotlib.rc("axes", unicode_minus=False) |
|
|
| _setup_font() |
|
|
|
|
| def build_mlp(n_t): |
| """mlp_kfold.py์ build_mlp์ ๋์ผ ๊ตฌ์กฐ (๊ฐ์ค์น ๋ก๋์ฉ).""" |
| 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) |
| out = layers.Dense(n_t * N_OUTPUTS, activation="linear", name="curve")(x) |
| return keras.Model(inp, out, name="MLP_fold") |
|
|
|
|
| @st.cache_resource |
| def load_folds(): |
| """folds/fold*/ ์ ๊ฐ์ค์น + ์ค์ผ์ผ๋ฌ๋ฅผ ๋ชจ๋ ๋ก๋.""" |
| if not FOLDS_DIR.exists(): |
| raise FileNotFoundError( |
| f"folds ํด๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.\n๊ฒฝ๋ก: {FOLDS_DIR.resolve()}\n\n" |
| "๋จผ์ ํ์ต์ ์คํํ์ธ์:\n python mlp_kfold.py" |
| ) |
| fold_dirs = sorted([d for d in FOLDS_DIR.iterdir() |
| if d.is_dir() and d.name.startswith("fold")]) |
| if not fold_dirs: |
| raise FileNotFoundError(f"fold ๋ชจ๋ธ์ด ์์ต๋๋ค: {FOLDS_DIR.resolve()}") |
|
|
| |
| sc0 = np.load(fold_dirs[0] / "scalers.npz") |
| time_arr = sc0["time_arr"] |
| n_t = len(time_arr) |
|
|
| folds = [] |
| for fd in fold_dirs: |
| sc = np.load(fd / "scalers.npz") |
| model = build_mlp(n_t) |
| model.load_weights(str(fd / "model.weights.h5")) |
| folds.append({ |
| "name": fd.name, |
| "model": model, |
| "param_mean": sc["param_mean"], |
| "param_std": sc["param_std"], |
| "param_log_mask": sc["param_log_mask"].astype(bool), |
| "out_mean": sc["out_mean"], |
| "out_std": sc["out_std"], |
| }) |
| return folds, time_arr, n_t |
|
|
|
|
| def preprocess(params, fold): |
| lm = fold["param_log_mask"] |
| pt = params.copy().astype(np.float64) |
| pt[lm] = np.log10(np.clip(pt[lm], 1e-300, None)) |
| return ((pt - fold["param_mean"]) / fold["param_std"]).astype(np.float32) |
|
|
|
|
| def predict_ensemble(folds, params, n_t): |
| """ |
| ๊ฐ fold๋ก ์์ธก ํ ์คํ. |
| ๋ฐํ: preds (n_folds, n_t, N_OUTPUTS) |
| """ |
| preds = [] |
| for fold in folds: |
| x = preprocess(params, fold).reshape(1, -1) |
| p = fold["model"](x, training=False).numpy().reshape(n_t, N_OUTPUTS) |
| p = p * fold["out_std"] + fold["out_mean"] |
| preds.append(p) |
| return np.stack(preds, axis=0) |
|
|
|
|
| def check_range(params): |
| warnings = [] |
| for name, val in zip(PARAM_NAMES, params): |
| lo, hi = PARAM_RANGES[name] |
| if val < lo or val > hi: |
| warnings.append(f"**{name}** = {val:.3e} (ํ์ฉ: {lo:.1e} ~ {hi:.1e})") |
| return warnings |
|
|
|
|
| def plot_combined(mean, std, time_arr): |
| fig, ax = plt.subplots(figsize=(12, 5)) |
| for k, name in enumerate(OUTPUT_NAMES): |
| ax.plot(time_arr, mean[:, k], color=COLORS[k], lw=2.5, label=name) |
| ax.fill_between(time_arr, mean[:, k] - std[:, k], mean[:, k] + std[:, k], |
| color=COLORS[k], alpha=0.18) |
| ax.set_xlabel("Time (min)", fontsize=12) |
| ax.set_ylabel("๋๋ (%)", fontsize=12) |
| ax.set_title("์ฝ๋ฌผ๋ํ ์์ธก", fontsize=13, fontweight="bold") |
| ax.set_xlim(time_arr.min(), time_arr.max()) |
| ax.set_ylim(bottom=0) |
| ax.legend(fontsize=11) |
| ax.grid(True, alpha=0.3) |
| plt.tight_layout() |
| return fig |
|
|
|
|
| def plot_subplots(mean, std, time_arr): |
| fig, axes = plt.subplots(2, 2, figsize=(14, 7)) |
| axes = axes.flatten() |
| for k, name in enumerate(OUTPUT_NAMES): |
| ax = axes[k] |
| ax.plot(time_arr, mean[:, k], color=COLORS[k], lw=2.5) |
| ax.fill_between(time_arr, mean[:, k] - std[:, k], mean[:, k] + std[:, k], |
| color=COLORS[k], alpha=0.18) |
| ax.set_title(name, fontsize=12, fontweight="bold") |
| ax.set_xlabel("Time (min)", fontsize=10) |
| ax.set_ylabel("๋๋ (%)", fontsize=10) |
| ax.set_xlim(time_arr.min(), time_arr.max()) |
| ax.set_ylim(bottom=0) |
| ax.grid(True, alpha=0.3) |
| plt.suptitle("๊ตฌํ๋ณ ๋๋-์๊ฐ ๊ณก์ (ยฑ ํธ์ฐจ)", fontsize=13, y=1.01) |
| plt.tight_layout() |
| return fig |
|
|
|
|
| st.set_page_config(page_title="PK Surrogate โ K-Fold Ensemble", page_icon="๐", layout="wide") |
| st.title("๐ ํผํ์ฃผ์ฌ ์ฝ๋ฌผ๋ํ ์์ธก ๋ชจ๋ธ") |
| st.markdown("10๊ฐ ํ๋ผ๋ฏธํฐ โ 4๊ตฌํ ๋๋-์๊ฐ ๊ณก์ (0~72์๊ฐ) | " |
| "์์์ fold ๊ฐ ํธ์ฐจ(์์ธก ์ ๋ขฐ๋)") |
|
|
| try: |
| folds, time_arr, n_t = load_folds() |
| except FileNotFoundError as e: |
| st.error(str(e)) |
| st.stop() |
| except Exception as e: |
| st.error(f"๋ก๋ฉ ์คํจ: {e}") |
| st.stop() |
|
|
| st.sidebar.header("๐ ํ๋ผ๋ฏธํฐ ์
๋ ฅ") |
| st.sidebar.markdown("---") |
| input_params = [] |
| for i, name in enumerate(PARAM_NAMES): |
| dv = float(DEFAULT_VALUES[i]) |
| lo, hi = PARAM_RANGES[name] |
| fmt = "%.2e" if (abs(dv) < 1e-3 or abs(dv) > 1e3) else "%.4f" |
| val = st.sidebar.number_input( |
| label=f"{name} ({lo:.1e} ~ {hi:.1e})", |
| value=dv, format=fmt, key=f"p_{i}", |
| ) |
| input_params.append(val) |
| st.sidebar.markdown("---") |
| predict_btn = st.sidebar.button("๐ฎ ์์ธกํ๊ธฐ", type="primary", use_container_width=True) |
|
|
| with st.expander("โน๏ธ ๋ชจ๋ธ ์ ๋ณด", expanded=False): |
| c1, c2, c3, c4 = st.columns(4) |
| c1.metric("๋ชจ๋ธ", "MLP") |
| c2.metric("๊ตฌ์กฐ", "[128,256,256,128]") |
| c3.metric("Dropout", "0.0 (L2=1e-5)") |
| c4.metric("Fold ์", f"{len(folds)}๊ฐ") |
|
|
| if predict_btn: |
| params = np.array(input_params, dtype=np.float64) |
|
|
| oor = check_range(params) |
| if oor: |
| st.warning( |
| "โ ๏ธ **์ธ์ฝ ๊ฒฝ๊ณ **: ์๋ ํ๋ผ๋ฏธํฐ๊ฐ ํ์ต ๋ฒ์๋ฅผ ๋ฒ์ด๋ฌ์ต๋๋ค. " |
| "์์ธก ์ ๋ขฐ๋๊ฐ ๋ฎ์ ์ ์์ต๋๋ค.\n\n" |
| + "\n".join(f"- {w}" for w in oor) |
| ) |
|
|
| with st.spinner("์์ธก ์ค..."): |
| preds = predict_ensemble(folds, params, n_t) |
| mean = preds.mean(axis=0) |
| std = preds.std(axis=0) |
|
|
| |
| rel_spread = float(std.mean()) |
| if rel_spread > 1.0: |
| st.warning( |
| f"โ ๏ธ **์์ธก ๋ถํ์ค์ฑ ๋์**: fold ๊ฐ ํ๊ท ํธ์ฐจ๊ฐ {rel_spread:.2f}%p์
๋๋ค. " |
| "์ด ํ๋ผ๋ฏธํฐ ์์ญ์ ํ์ต ๋ฐ์ดํฐ๊ฐ ์ฑ๊ฒจ ์ ๋ขฐ๋๊ฐ ๋ฎ์ ์ ์์ต๋๋ค. " |
| "COMSOL ์ง์ ๊ฒ์ฆ์ ๊ถ์ฅํฉ๋๋ค." |
| ) |
|
|
| st.markdown("---") |
| st.subheader("๐ 72์๊ฐ ์ต์ข
๊ฐ ์์ฝ") |
| |
| total_mean = mean.sum(axis=1) |
| total_std = std.sum(axis=1) |
| cols = st.columns(5) |
| cols[0].metric( |
| label="c_total (์ดํฉ)", |
| value=f"{total_mean[-1]:.2f}%", |
| delta=f"max: {total_mean.max():.2f}%", |
| delta_color="off", |
| ) |
| for k, name in enumerate(OUTPUT_NAMES): |
| cols[k + 1].metric( |
| label=name, |
| value=f"{mean[-1, k]:.2f}%", |
| delta=f"ยฑ{std[-1, k]:.2f}%p (fold ํธ์ฐจ)", |
| delta_color="off", |
| ) |
|
|
| st.markdown("---") |
| st.subheader("๐ ๋๋-์๊ฐ ๊ณก์ ") |
| tab1, tab2 = st.tabs(["์ ์ฒด ๋น๊ต", "๊ตฌํ๋ณ ์์ธ"]) |
| with tab1: |
| fig1 = plot_combined(mean, std, time_arr) |
| st.pyplot(fig1); plt.close(fig1) |
| with tab2: |
| fig2 = plot_subplots(mean, std, time_arr) |
| st.pyplot(fig2); plt.close(fig2) |
|
|
| st.markdown("---") |
| st.subheader("๐ PK ์งํ") |
| |
| auc_vals = np.array([_trapz(mean[:, k], time_arr) for k in range(N_OUTPUTS)]) |
| auc_total = auc_vals.sum() + 1e-12 |
| rows = [] |
| for k, name in enumerate(OUTPUT_NAMES): |
| curve = mean[:, k] |
| rows.append({ |
| "๊ตฌํ": name, |
| "Cmax (%)": f"{curve.max():.2f}", |
| "Tmax (min)": f"{time_arr[int(np.argmax(curve))]:.0f}", |
| "%AUC": f"{auc_vals[k] / auc_total * 100:.1f}%", |
| "72hr (%)": f"{curve[-1]:.2f}", |
| }) |
| st.table(rows) |
|
|
| |
| st.markdown("---") |
| st.subheader("๐ฅ ์์ ๋ณ ๋๋๊ฐ ๋ค์ด๋ก๋") |
| st.markdown("๋ชจ๋ธ์ด ์์ธกํ 42๊ฐ ์์ ์ ๊ตฌํ๋ณ ๋๋๊ฐ์ ์์
๋ก ์ ์ฅํฉ๋๋ค.") |
|
|
| df_out = pd.DataFrame({"time_min": time_arr, "time_hr": time_arr / 60.0}) |
| for k, name in enumerate(OUTPUT_NAMES): |
| df_out[name] = mean[:, k] |
|
|
| buf = io.BytesIO() |
| with pd.ExcelWriter(buf, engine="openpyxl") as writer: |
| df_out.to_excel(writer, index=False, sheet_name="๋๋_์๊ฐ๋ณ") |
| |
| pd.DataFrame({"parameter": PARAM_NAMES, "value": input_params} |
| ).to_excel(writer, index=False, sheet_name="์
๋ ฅ_ํ๋ผ๋ฏธํฐ") |
| buf.seek(0) |
|
|
| st.download_button( |
| label="โฌ๏ธ ์์
๋ค์ด๋ก๋ (.xlsx)", |
| data=buf, |
| file_name="๋๋_์๊ฐ๋ณ_์์ธก.xlsx", |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| use_container_width=False, |
| ) |
| with st.expander("๋ฏธ๋ฆฌ๋ณด๊ธฐ"): |
| st.dataframe(df_out.style.format({ |
| "time_min": "{:.0f}", "time_hr": "{:.2f}", |
| **{name: "{:.3f}" for name in OUTPUT_NAMES}, |
| }), use_container_width=True) |
|
|
| with st.expander("๐ ์
๋ ฅ ํ๋ผ๋ฏธํฐ ํ์ธ"): |
| st.json({ |
| name: (f"{val:.3e}" if (abs(val) < 1e-3 or abs(val) > 1e3) else f"{val:.4f}") |
| for name, val in zip(PARAM_NAMES, input_params) |
| }) |
|
|
| else: |
| st.info("๐ ์ผ์ชฝ ์ฌ์ด๋๋ฐ์์ ํ๋ผ๋ฏธํฐ๋ฅผ ์
๋ ฅํ๊ณ **์์ธกํ๊ธฐ** ๋ฒํผ์ ๋๋ฅด์ธ์.") |
| st.markdown(""" |
| ### ์์ธก ์ ๋ขฐ๋ |
| fold ๊ฐ **ํธ์ฐจ(์์)**๊ฐ ๊ทธ ํ๋ผ๋ฏธํฐ ์์ญ์์์ ์์ธก ์ ๋ขฐ๋๋ฅผ ๋ํ๋
๋๋ค. |
| ํธ์ฐจ๊ฐ ํฌ๋ฉด ํ์ต ๋ฐ์ดํฐ๊ฐ ์ฑ๊ธด ์์ญ์ด๋ผ ์ ๋ขฐ๋๊ฐ ๋ฎ์ต๋๋ค. |
| |
| ### ์ถ๋ ฅ ๊ตฌํ |
| | ๊ตฌํ | ์ค๋ช
| |
| |------|------| |
| | c_lymph | ๋ฆผํ๊ด ๋ฐฐ์ถ | |
| | c_vessel | ํ๊ด ๋ฐฐ์ถ | |
| | c_decay | ๋ถํด | |
| | c_ecm | ์ธํฌ์ธ๊ธฐ์ง ์๋ฅ | |
| |
| ### ์ฃผ์์ฌํญ |
| - ํ๋ผ๋ฏธํฐ ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด **์ธ์ฝ ๊ฒฝ๊ณ ** |
| - fold ํธ์ฐจ๊ฐ ํฌ๋ฉด **๋ถํ์ค์ฑ ๊ฒฝ๊ณ ** (COMSOL ์ง์ ๊ฒ์ฆ ๊ถ์ฅ) |
| """) |
|
|
| if __name__ == "__main__": |
| import os, subprocess, sys |
| if os.environ.get("STREAMLIT_RUNNING") != "1": |
| env = os.environ.copy() |
| env["STREAMLIT_RUNNING"] = "1" |
| subprocess.run([sys.executable, "-m", "streamlit", "run", __file__], env=env) |