comsol_surrogate_model / src /kfold_mlp_app.py
guminhong's picture
Upload 3 files
d1ebc0e verified
Raw
History Blame Contribute Delete
12.7 kB
"""
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
# === ๋ชจ๋ธ ๊ตฌ์กฐ (mlp_kfold.py์™€ ๋ฐ˜๋“œ์‹œ ๋™์ผ) ===
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()}")
# ์‹œ๊ฐ„์ถ•์€ fold ๊ณตํ†ต (์ฒซ fold ๊ธฐ์ค€)
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) # (n_folds, n_t, 4)
mean = preds.mean(axis=0)
std = preds.std(axis=0)
# fold ๊ฐ„ ํŽธ์ฐจ๊ฐ€ ํฌ๋ฉด ์‹ ๋ขฐ๋„ ๋‚ฎ์Œ ๊ฒฝ๊ณ 
rel_spread = float(std.mean())
if rel_spread > 1.0: # ํ‰๊ท  ํŽธ์ฐจ 1%p ์ด์ƒ์ด๋ฉด
st.warning(
f"โš ๏ธ **์˜ˆ์ธก ๋ถˆํ™•์‹ค์„ฑ ๋†’์Œ**: fold ๊ฐ„ ํ‰๊ท  ํŽธ์ฐจ๊ฐ€ {rel_spread:.2f}%p์ž…๋‹ˆ๋‹ค. "
"์ด ํŒŒ๋ผ๋ฏธํ„ฐ ์˜์—ญ์€ ํ•™์Šต ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ฒจ ์‹ ๋ขฐ๋„๊ฐ€ ๋‚ฎ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
"COMSOL ์ง์ ‘ ๊ฒ€์ฆ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค."
)
st.markdown("---")
st.subheader("๐Ÿ“ˆ 72์‹œ๊ฐ„ ์ตœ์ข…๊ฐ’ ์š”์•ฝ")
# ์ด๋†๋„(4๊ตฌํš ํ•ฉ)๋ฅผ ๋งจ ์•ž์— ์ถ”๊ฐ€
total_mean = mean.sum(axis=1) # (n_t,) ์‹œ์ ๋ณ„ 4๊ตฌํš ํ•ฉ
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: ๊ฐ ๊ตฌํš AUC๊ฐ€ ์ „์ฒด AUC ํ•ฉ์—์„œ ์ฐจ์ง€ํ•˜๋Š” ๋น„์œจ (ํ•ฉ 100%)
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)