Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 3 additions & 19 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@


def get_venv_python() -> Path:
"""
현재 프로젝트의 .venv 내 Python 실행 파일 경로를 반환합니다.
"""
base_path = Path.cwd() # 현재 작업 경로
base_path = Path.cwd()

# OS에 따른 실행 파일 경로 분기
if platform.system() == "Windows":
venv_python = base_path / ".venv" / "Scripts" / "python.exe"
else:
Expand All @@ -26,22 +22,16 @@ def get_venv_python() -> Path:


def find_exp_folder(exp_num: int, runs_dir_path: Path = Path("./runs")) -> Path:
"""
runs 폴더 내에서 exp{num} 또는 exp0{num}으로 시작하는 폴더를 찾습니다.
"""
# runs 폴더 존재 확인
"""Folder under runs/ whose name starts with exp{num}_ or exp0{num}_."""
if not runs_dir_path.exists():
print(f"❌ [Error] '{runs_dir_path}' 디렉토리가 존재하지 않습니다.")
sys.exit(1)

# 검색할 접두사 패턴 (예: exp5_, exp05_)
target_prefixes = [f"exp{exp_num}_", f"exp{exp_num:02d}_"]

# iterdir()로 폴더 순회하며 찾기
found_folder = None
for item in runs_dir_path.iterdir():
if item.is_dir():
# 폴더 이름이 접두사 중 하나로 시작하는지 확인
if any(item.name.startswith(prefix) for prefix in target_prefixes):
found_folder = item
break
Expand All @@ -55,9 +45,6 @@ def find_exp_folder(exp_num: int, runs_dir_path: Path = Path("./runs")) -> Path:


def run_main(target_folder: Path):
"""
찾은 폴더 내부의 main.py를 가상환경 Python으로 실행합니다.
"""
venv_python = get_venv_python()
target_script = target_folder / "main.py"

Expand All @@ -70,10 +57,9 @@ def run_main(target_folder: Path):
print("-" * 50)

try:
# subprocess에는 Path 객체를 문자열로 변환(str)해서 넘기는 것이 안전합니다.
subprocess.run(
[str(venv_python), "main.py"],
cwd=target_folder, # 작업 디렉토리를 해당 실험 폴더로 변경
cwd=target_folder,
check=True,
)
except subprocess.CalledProcessError as e:
Expand All @@ -87,8 +73,6 @@ def run_main(target_folder: Path):

args = parser.parse_args()

# 1. 대상 폴더 찾기
target_folder = find_exp_folder(args.exp_num)

# 2. 실행
run_main(target_folder)
15 changes: 0 additions & 15 deletions runs/exp01_quan_class/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ def __init__(self, thickness_bins: np.ndarray | None = None, roughness_bins: np.
self.roughness_bins = np.linspace(0.0, 10.0, 21) if roughness_bins is None else np.asarray(roughness_bins)
self.sld_bins = np.linspace(1.0, 6.0, 26) if sld_bins is None else np.asarray(sld_bins)

# --------------------------
# Persistence
# --------------------------
def state_dict(self):
return {
"thickness_bins": self.thickness_bins.astype(float).tolist(),
Expand All @@ -49,9 +46,6 @@ def load_state_dict(self, state: dict):
self.roughness_bins = np.asarray(state["roughness_bins"], dtype=float)
self.sld_bins = np.asarray(state["sld_bins"], dtype=float)

# --------------------------
# Fitting helper
# --------------------------
def fit_from_dataset(self, h5_path: str, sample_n: int = 1000, rng_seed: int = 0):
"""Estimate reasonable bins from real dataset values (sample subset)."""
rng = np.random.default_rng(rng_seed)
Expand Down Expand Up @@ -83,9 +77,6 @@ def fit_from_dataset(self, h5_path: str, sample_n: int = 1000, rng_seed: int = 0
self.roughness_bins = np.linspace(max(0.0, rvals.min()), rvals.max(), 21)
self.sld_bins = np.linspace(svals.min(), svals.max(), 26)

# --------------------------
# Quantize API
# --------------------------
@staticmethod
def _quantize_array(vals: np.ndarray, bins: np.ndarray) -> np.ndarray:
"""Vectorized quantization. Returns indices clipped to valid range [0, len(bins)-2]."""
Expand Down Expand Up @@ -160,9 +151,6 @@ def __init__(
def __len__(self) -> int:
return int(self._length)

# --------------------------
# HDF5 access helpers
# --------------------------
def _ensure_open(self) -> h5py.File:
"""Open file per-process (safe with num_workers spawn/fork)."""
if self._hf is None:
Expand Down Expand Up @@ -190,9 +178,6 @@ def _estimate_stats(self, n_sample: int = 1000) -> tuple[float, float]:
return 0.0, 1.0
return float(arr.mean()), float(arr.std() if arr.std() > 0 else 1.0)

# --------------------------
# Data conversion
# --------------------------
def _process_reflectivity(self, refl: np.ndarray) -> np.ndarray:
"""Apply log-scaling, downsampling, normalization and return float32 array."""
arr = np.asarray(refl, dtype=float).ravel()
Expand Down
15 changes: 2 additions & 13 deletions runs/exp01_quan_class/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(
checkpoint_path: Path | str,
batch_size: int = 64,
device: str | torch.device = None,
num_workers: int = 0, # Windows에서는 0~2 권장
num_workers: int = 0, # 0-2 recommended on Windows
pin_memory: bool = True,
):
self.h5_path = Path(h5_path)
Expand Down Expand Up @@ -50,7 +50,6 @@ def __init__(
self.model = XRRClassifier(q_len=self.q_len, n_layers=self.n_layers, n_th_bins=self.n_th, n_rg_bins=self.n_rg, n_sld_bins=self.n_sld).to(
self.device
)
# --- Safe checkpoint loading ---
if self.checkpoint_path and self.checkpoint_path.is_file():
try:
state = torch.load(self.checkpoint_path, map_location=self.device)
Expand All @@ -67,7 +66,6 @@ def __init__(
self.ce = nn.CrossEntropyLoss()

def evaluate(self, use_amp: bool = False, logits_clip: float = 50.0):
"""모든 평가 데이터를 계산하고 결과 저장"""
all_tgt_th, all_tgt_rg, all_tgt_sld = [], [], []
all_pred_th, all_pred_rg, all_pred_sld = [], [], []
all_losses = []
Expand All @@ -80,33 +78,28 @@ def evaluate(self, use_amp: bool = False, logits_clip: float = 50.0):
refl = refl.to(self.device)
labels = labels.to(self.device)

# --- AMP 실제 적용 ---
if use_amp and self.device.type == "cuda":
with torch.amp.autocast("cuda"):
th_logits, rg_logits, sld_logits = self.model(refl)
else:
th_logits, rg_logits, sld_logits = self.model(refl)

# --- logits clipping (학습과 동일하게) ---
if logits_clip is not None:
th_logits = th_logits.clamp(-logits_clip, logits_clip)
rg_logits = rg_logits.clamp(-logits_clip, logits_clip)
sld_logits = sld_logits.clamp(-logits_clip, logits_clip)

# --- target ---
tgt_th = labels[:, :, 0]
tgt_rg = labels[:, :, 1]
tgt_sld = labels[:, :, 2]

# --- Loss 계산(CrossEntropy 조합) ---
loss = (
self.ce(th_logits.reshape(-1, th_logits.shape[-1]), tgt_th.reshape(-1))
+ self.ce(rg_logits.reshape(-1, rg_logits.shape[-1]), tgt_rg.reshape(-1))
+ self.ce(sld_logits.reshape(-1, sld_logits.shape[-1]), tgt_sld.reshape(-1))
)
all_losses.append(loss.item())

# --- 예측 저장 ---
all_tgt_th.append(tgt_th.cpu().numpy())
all_tgt_rg.append(tgt_rg.cpu().numpy())
all_tgt_sld.append(tgt_sld.cpu().numpy())
Expand All @@ -117,7 +110,6 @@ def evaluate(self, use_amp: bool = False, logits_clip: float = 50.0):

pbar.set_postfix({"loss": f"{loss.item():.4f}"})

# --- Flatten ---
self.all_tgt_th = np.concatenate(all_tgt_th).ravel()
self.all_tgt_rg = np.concatenate(all_tgt_rg).ravel()
self.all_tgt_sld = np.concatenate(all_tgt_sld).ravel()
Expand All @@ -128,7 +120,6 @@ def evaluate(self, use_amp: bool = False, logits_clip: float = 50.0):

self.all_losses = all_losses

# --- Accuracy ---
self.acc_th = (self.all_pred_th == self.all_tgt_th).mean()
self.acc_rg = (self.all_pred_rg == self.all_tgt_rg).mean()
self.acc_sld = (self.all_pred_sld == self.all_tgt_sld).mean()
Expand Down Expand Up @@ -203,11 +194,9 @@ def plot_scatter(self, use_hexbin: bool = False):
else:
plt.scatter(tgt, pred, alpha=0.2, s=8)

# y = x선 (이상적 예측)
tmin, tmax = tgt.min(), tgt.max()
plt.plot([tmin, tmax], [tmin, tmax], "r--", linewidth=1.5, label="Ideal")

# 추세선 (회귀)
coef = np.polyfit(tgt, pred, 1)
poly = np.poly1d(coef)
plt.plot([tmin, tmax], poly([tmin, tmax]), "b-", linewidth=1.2, label=f"Fit: y={coef[0]:.2f}x+{coef[1]:.2f}")
Expand Down Expand Up @@ -249,7 +238,7 @@ def plot_correlation(self):
h5_path=Path(os.environ["DATA"], r"XRR_AI\data\p300o6_raw.h5"),
checkpoint_path=Path(os.environ["DATA"], r"XRR_AI\model\xrr_model.pt"),
batch_size=64,
num_workers=0, # Windows 안정성
num_workers=0, # 0 for stability on Windows
)
metrics = evaluator.evaluate(use_amp=True)
print(f"Validation Loss: {metrics['loss']:.4f}")
Expand Down
7 changes: 0 additions & 7 deletions runs/exp01_quan_class/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,6 @@ def forward(self, x):
return logits_th, logits_rg, logits_sld


# -------------------------
# Loss helper
# -------------------------
def multitask_loss(
logits_th,
logits_rg,
Expand Down Expand Up @@ -151,9 +148,6 @@ def accuracy_from_logits(logits, targets):
return correct.mean().item()


# -------------------------
# Training / Validation
# -------------------------
def train_epoch(
model: nn.Module,
loader: torch.utils.data.DataLoader,
Expand Down Expand Up @@ -229,7 +223,6 @@ def validate_epoch(

logits_th, logits_rg, logits_sld = model(refl)

# --- 안정성 확보: logits clipping ---
if logits_clip is not None:
logits_th = logits_th.clamp(-logits_clip, logits_clip)
logits_rg = logits_rg.clamp(-logits_clip, logits_clip)
Expand Down
1 change: 0 additions & 1 deletion runs/exp01_quan_class/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ def train(self, save_every: int = 5):
total_loss += loss.item()
last_parts = {"th": loss_th.item(), "rg": loss_rg.item(), "sld": loss_sld.item()}

# tqdm에 실시간 표시
pbar.set_postfix({**last_parts, "loss": f"{loss.item():.4f}"})

avg_train_loss = total_loss / len(self.train_loader)
Expand Down
1 change: 0 additions & 1 deletion runs/exp02_one_layer/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
from pathlib import Path

# ==================== 설정 (하나의 파일에서 모두 관리) ====================
CONFIG = {
"exp_name": "footprint2",
"base_dir": Path(os.environ["DATA"], r"XRR_AI\data\one_layer"),
Expand Down
14 changes: 2 additions & 12 deletions runs/exp02_one_layer/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@


class XRR1LayerDataset(Dataset):
"""1-layer 전용 Dataset"""

def __init__(self, h5_file: str | Path, stats_file: str | Path, mode: str = "train", val_ratio: float = 0.2):
self.h5_path = Path(h5_file)
self.stats_path = stats_file
Expand All @@ -23,7 +21,6 @@ def __init__(self, h5_file: str | Path, stats_file: str | Path, mode: str = "tra

self.n_total = len(self.thickness)

# 명확한 분할 (Train: 70%, Val: 20%, Test: 10%)
test_ratio = 0.1
train_ratio = 1.0 - val_ratio - test_ratio

Expand All @@ -40,9 +37,8 @@ def __init__(self, h5_file: str | Path, stats_file: str | Path, mode: str = "tra
self._setup_normalization()

def _setup_normalization(self):
"""정규화 파라미터 설정"""
if self.mode == "train":
# 통계 계산 (train 부분만) - val_ratio에 따라 계산된 실제 분할 경계 사용
# statistics from the train split only
train_indices = range(self.train_end)

R_log = np.log10(np.maximum(self.reflectivity[train_indices], 1e-15))
Expand All @@ -55,7 +51,6 @@ def _setup_normalization(self):
self.param_mean = np.mean(params, axis=0)
self.param_std = np.std(params, axis=0)

# torch tensor로 저장하여 torch.load 문제 해결
torch.save(
{
"refl_mean": torch.from_numpy(self.refl_mean),
Expand All @@ -68,7 +63,6 @@ def _setup_normalization(self):
print(f"정규화 파라미터 저장: {self.stats_path}")

else:
# 저장된 tensor 로드
stats = torch.load(self.stats_path)
self.refl_mean = stats["refl_mean"].numpy()
self.refl_std = stats["refl_std"].numpy()
Expand All @@ -81,30 +75,26 @@ def __len__(self):
def __getitem__(self, idx):
real_idx = self.indices[idx]

# 반사율 처리
R = self.reflectivity[real_idx]
R_log = np.log10(np.maximum(R, 1e-15))
R_norm = (R_log - self.refl_mean) / self.refl_std

# 파라미터
params = np.array([self.thickness[real_idx], self.roughness[real_idx], self.sld[real_idx]], dtype=np.float32)

params_norm = (params - self.param_mean) / self.param_std

return (torch.from_numpy(R_norm).float(), torch.from_numpy(params_norm).float())

def denormalize_params(self, params_norm: torch.Tensor | np.ndarray) -> np.ndarray:
"""모델 출력(정규화된)을 실제 물리 파라미터 스케일로 복원."""
"""Map normalized model outputs back to physical parameter units."""
if isinstance(params_norm, torch.Tensor):
params_norm = params_norm.detach().cpu().numpy()

params = params_norm * self.param_std + self.param_mean
return params

def normalize_reflectivity(self, R_real: np.ndarray) -> torch.Tensor:
"""실험 R 데이터를 모델 입력용으로 정규화."""
R_log = np.log10(np.maximum(R_real, 1e-15))

# 학습 시 저장된 mean/std 사용
R_norm = (R_log - self.refl_mean) / self.refl_std
return torch.from_numpy(R_norm).float()
10 changes: 1 addition & 9 deletions runs/exp02_one_layer/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@

def evaluate(model, test_loader: DataLoader, stats_path: Path | str, report_file: Path | None = None, save_plot: bool = True):
"""
모델 평가: 역정규화 후 물리적 오류 계산
Evaluate the model; errors are computed after denormalization, in physical units.
"""
stats_path = Path(stats_path)
device = next(model.parameters()).device

# 정규화 파라미터 로드
stats = torch.load(stats_path)
param_mean = stats["param_mean"]
param_std = stats["param_std"]
Expand All @@ -32,11 +31,9 @@ def evaluate(model, test_loader: DataLoader, stats_path: Path | str, report_file
all_preds_norm.append(pred_norm)
all_targets_norm.append(params_norm)

# 역정규화
preds = torch.cat(all_preds_norm) * param_std + param_mean
targets = torch.cat(all_targets_norm) * param_std + param_mean

# 오류 계산
errors = preds - targets
mae = torch.mean(torch.abs(errors), dim=0)
rmse = torch.sqrt(torch.mean(errors**2, dim=0))
Expand All @@ -49,7 +46,6 @@ def evaluate(model, test_loader: DataLoader, stats_path: Path | str, report_file
print(f"{name:15s}: MAE={mae[i]:7.3f} | RMSE={rmse[i]:7.3f}")
print("=" * 50)

# 시각화
if save_plot:
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

Expand All @@ -75,17 +71,13 @@ def evaluate(model, test_loader: DataLoader, stats_path: Path | str, report_file


def load_checkpoint_and_evaluate(test_loader: DataLoader, checkpoint_path: Path, stats_path: Path, report_file: Path):
"""체크포인트 로드 및 평가"""
# 체크포인트 로드
ckpt = torch.load(checkpoint_path, map_location="cpu")
config = ckpt["config"]["model_args"]

# 모든 인자로 모델 생성
model = XRR1DRegressor(**config)
model.load_state_dict(ckpt["model_state_dict"])
model = model.to(torch.device("cuda" if torch.cuda.is_available() else "cpu"))

# 평가
evaluate(model, test_loader, stats_path, report_file)


Expand Down
Loading
Loading