Improve split strategy for more reliable training evaluation
Group samples by identical inputs before splitting, add target-aware stratification options, and cover the behavior with tests so repeated-input rows no longer leak across train, validation, and test sets. Made-with: Cursor
This commit is contained in:
@@ -45,7 +45,7 @@
|
|||||||
`v_pi_min <= V_pi <= v_pi_max`(默认 **`[0, 500]`**)。**区间之外整行剔除**。
|
`v_pi_min <= V_pi <= v_pi_max`(默认 **`[0, 500]`**)。**区间之外整行剔除**。
|
||||||
该步骤专门针对「以最后一列 `V_pi` 为正常范围」的需求。
|
该步骤专门针对「以最后一列 `V_pi` 为正常范围」的需求。
|
||||||
4. **(可选)严格正电压**:`remove_nonpositive_vpi: true` 时,在区间过滤之后再删除 `V_pi <= 0`(若需保留 `V_pi = 0` 且仍在 `[0,500]` 内,请保持为 `false`)。
|
4. **(可选)严格正电压**:`remove_nonpositive_vpi: true` 时,在区间过滤之后再删除 `V_pi <= 0`(若需保留 `V_pi = 0` 且仍在 `[0,500]` 内,请保持为 `false`)。
|
||||||
5. **后续步骤**:train/val/test 划分、(可选)训练集离群策略、仅在训练集上拟合 `StandardScaler` 等,与原先一致。
|
5. **后续步骤**:默认采用**按 8 个输入字段分组**的 train/val/test 切分,避免「同输入异输出」同时落入不同集合;再按 `split_stratify_target`(默认 `V_pi`)做组级近似分层;之后才做(可选)训练集离群策略与仅在训练集上拟合 `StandardScaler`。
|
||||||
|
|
||||||
清洗前会在日志与 `data_report.md` 中报告:给定 `[v_pi_min, v_pi_max]` 下 **`V_pi` 越界行数**、重复样本、同输入异输出等统计,便于核对。
|
清洗前会在日志与 `data_report.md` 中报告:给定 `[v_pi_min, v_pi_max]` 下 **`V_pi` 越界行数**、重复样本、同输入异输出等统计,便于核对。
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ pytest -q tests/test_smoke.py
|
|||||||
主要字段:
|
主要字段:
|
||||||
|
|
||||||
- **数据与清洗**:`data_path`、`remove_duplicate_rows`、**`filter_v_pi_range` / `v_pi_min` / `v_pi_max`**(默认按 **`V_pi ∈ [0, 500]`** 剔除越界行,对应 txt **第 11 列**)、`remove_nonpositive_vpi`、`outlier_strategy`(`none` / `iqr` / `zscore` / `quantile_clip`)及 `outlier_apply_to`(`targets` / `all`)。
|
- **数据与清洗**:`data_path`、`remove_duplicate_rows`、**`filter_v_pi_range` / `v_pi_min` / `v_pi_max`**(默认按 **`V_pi ∈ [0, 500]`** 剔除越界行,对应 txt **第 11 列**)、`remove_nonpositive_vpi`、`outlier_strategy`(`none` / `iqr` / `zscore` / `quantile_clip`)及 `outlier_apply_to`(`targets` / `all`)。
|
||||||
- **划分**:`split_ratios`、`random_seed`;先 shuffle 再切分;**仅在训练子集**上拟合标准化器;离群阈值(若启用)也在训练子集上统计。
|
- **划分**:`split_ratios`、`random_seed`、`split_mode`、`split_stratify_target`、`split_stratify_bins`。默认 `grouped_stratified`:先按 8 维输入分组,再按指定目标(默认 `V_pi`)做组级近似分层;也可切回 `random`。**仅在训练子集**上拟合标准化器;离群阈值(若启用)也在训练子集上统计。
|
||||||
- **模型**:`hidden_dims`、`batchnorm`、`dropout`、`residual`。
|
- **模型**:`hidden_dims`、`batchnorm`、`dropout`、`residual`。
|
||||||
- **训练**:`AdamW`、`lr`、`weight_decay`、`batch_size`、`epochs`、早停 `early_stopping_patience`。
|
- **训练**:`AdamW`、`lr`、`weight_decay`、`batch_size`、`epochs`、早停 `early_stopping_patience`。
|
||||||
- **调度器**:`cosine`(默认)或 `plateau`。
|
- **调度器**:`cosine`(默认)或 `plateau`。
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ data_path: data/dataset.txt
|
|||||||
|
|
||||||
split_ratios: [0.7, 0.15, 0.15] # train, val, test;可改为 [0.8, 0.1, 0.1]
|
split_ratios: [0.7, 0.15, 0.15] # train, val, test;可改为 [0.8, 0.1, 0.1]
|
||||||
random_seed: 42
|
random_seed: 42
|
||||||
|
# 切分策略:按 8 维输入分组,避免“同输入异输出”跨集合泄漏;再按目标分桶近似分层
|
||||||
|
split_mode: grouped_stratified # grouped_stratified | random
|
||||||
|
split_stratify_target: V_pi
|
||||||
|
split_stratify_bins: 10
|
||||||
|
|
||||||
remove_duplicate_rows: true
|
remove_duplicate_rows: true
|
||||||
|
|
||||||
@@ -32,7 +36,8 @@ model:
|
|||||||
hidden_dims: [200, 300, 350, 300, 200]
|
hidden_dims: [200, 300, 350, 300, 200]
|
||||||
output_dim: 3
|
output_dim: 3
|
||||||
batchnorm: false
|
batchnorm: false
|
||||||
dropout: 0.0
|
# 温和 dropout,实测略优于全 0(见 results/run_20260419_163305)
|
||||||
|
dropout: 0.05
|
||||||
residual: false
|
residual: false
|
||||||
|
|
||||||
optimizer:
|
optimizer:
|
||||||
@@ -55,7 +60,8 @@ training:
|
|||||||
loss:
|
loss:
|
||||||
type: huber # huber | weighted_mse
|
type: huber # huber | weighted_mse
|
||||||
huber_delta: 1.0
|
huber_delta: 1.0
|
||||||
target_weights: [1.0, 1.0, 1.0]
|
# BW_3dB, IL, V_pi;略加重 V_pi 以小幅提升其测试 R²
|
||||||
|
target_weights: [1.0, 1.0, 1.2]
|
||||||
|
|
||||||
# 总输出目录;每次训练会在其下创建 run_时间戳/
|
# 总输出目录;每次训练会在其下创建 run_时间戳/
|
||||||
output_dir: results
|
output_dir: results
|
||||||
|
|||||||
59
configs/mild_reg.yaml
Normal file
59
configs/mild_reg.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# 温和正则 + 略提高 V_pi 权重(在 default 基线上小幅改动,便于对比)
|
||||||
|
# 使用: python -m src.main train --config configs/mild_reg.yaml
|
||||||
|
|
||||||
|
data_path: data/dataset.txt
|
||||||
|
|
||||||
|
split_ratios: [0.7, 0.15, 0.15]
|
||||||
|
random_seed: 42
|
||||||
|
split_mode: grouped_stratified
|
||||||
|
split_stratify_target: V_pi
|
||||||
|
split_stratify_bins: 10
|
||||||
|
|
||||||
|
remove_duplicate_rows: true
|
||||||
|
|
||||||
|
outlier_strategy: none
|
||||||
|
outlier_apply_to: targets
|
||||||
|
outlier_config:
|
||||||
|
iqr_k: 1.5
|
||||||
|
zscore_threshold: 4.0
|
||||||
|
quantile_lower: 0.001
|
||||||
|
quantile_upper: 0.999
|
||||||
|
|
||||||
|
filter_v_pi_range: true
|
||||||
|
v_pi_min: 0.0
|
||||||
|
v_pi_max: 500.0
|
||||||
|
|
||||||
|
remove_nonpositive_vpi: false
|
||||||
|
|
||||||
|
model:
|
||||||
|
input_dim: 8
|
||||||
|
hidden_dims: [200, 300, 350, 300, 200]
|
||||||
|
output_dim: 3
|
||||||
|
batchnorm: false
|
||||||
|
dropout: 0.05
|
||||||
|
residual: false
|
||||||
|
|
||||||
|
optimizer:
|
||||||
|
name: adamw
|
||||||
|
lr: 0.001
|
||||||
|
weight_decay: 0.0001
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
type: cosine
|
||||||
|
plateau_factor: 0.5
|
||||||
|
plateau_patience: 10
|
||||||
|
plateau_min_lr: 1.0e-6
|
||||||
|
|
||||||
|
training:
|
||||||
|
batch_size: 128
|
||||||
|
epochs: 300
|
||||||
|
early_stopping_patience: 30
|
||||||
|
num_workers: 0
|
||||||
|
|
||||||
|
loss:
|
||||||
|
type: huber
|
||||||
|
huber_delta: 1.0
|
||||||
|
target_weights: [1.0, 1.0, 1.2]
|
||||||
|
|
||||||
|
output_dir: results
|
||||||
|
last_run_dir: null
|
||||||
59
configs/reg_vpi_try1.yaml
Normal file
59
configs/reg_vpi_try1.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# 实验:较强正则 + 提高 V_pi 损失权重(2026-04-19 试跑)
|
||||||
|
# 结果:早停偏早,test 整体差于 default 基线;仅作记录,日常训练请用 default.yaml
|
||||||
|
|
||||||
|
data_path: data/dataset.txt
|
||||||
|
|
||||||
|
split_ratios: [0.7, 0.15, 0.15]
|
||||||
|
random_seed: 42
|
||||||
|
split_mode: grouped_stratified
|
||||||
|
split_stratify_target: V_pi
|
||||||
|
split_stratify_bins: 10
|
||||||
|
|
||||||
|
remove_duplicate_rows: true
|
||||||
|
|
||||||
|
outlier_strategy: none
|
||||||
|
outlier_apply_to: targets
|
||||||
|
outlier_config:
|
||||||
|
iqr_k: 1.5
|
||||||
|
zscore_threshold: 4.0
|
||||||
|
quantile_lower: 0.001
|
||||||
|
quantile_upper: 0.999
|
||||||
|
|
||||||
|
filter_v_pi_range: true
|
||||||
|
v_pi_min: 0.0
|
||||||
|
v_pi_max: 500.0
|
||||||
|
|
||||||
|
remove_nonpositive_vpi: false
|
||||||
|
|
||||||
|
model:
|
||||||
|
input_dim: 8
|
||||||
|
hidden_dims: [200, 300, 350, 300, 200]
|
||||||
|
output_dim: 3
|
||||||
|
batchnorm: false
|
||||||
|
dropout: 0.15
|
||||||
|
residual: false
|
||||||
|
|
||||||
|
optimizer:
|
||||||
|
name: adamw
|
||||||
|
lr: 0.001
|
||||||
|
weight_decay: 0.0002
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
type: cosine
|
||||||
|
plateau_factor: 0.5
|
||||||
|
plateau_patience: 10
|
||||||
|
plateau_min_lr: 1.0e-6
|
||||||
|
|
||||||
|
training:
|
||||||
|
batch_size: 128
|
||||||
|
epochs: 300
|
||||||
|
early_stopping_patience: 30
|
||||||
|
num_workers: 0
|
||||||
|
|
||||||
|
loss:
|
||||||
|
type: huber
|
||||||
|
huber_delta: 1.0
|
||||||
|
target_weights: [1.0, 1.0, 1.75]
|
||||||
|
|
||||||
|
output_dir: results
|
||||||
|
last_run_dir: null
|
||||||
@@ -62,6 +62,9 @@ class AppConfig:
|
|||||||
data_path: str
|
data_path: str
|
||||||
split_ratios: List[float]
|
split_ratios: List[float]
|
||||||
random_seed: int
|
random_seed: int
|
||||||
|
split_mode: str
|
||||||
|
split_stratify_target: str
|
||||||
|
split_stratify_bins: int
|
||||||
remove_duplicate_rows: bool
|
remove_duplicate_rows: bool
|
||||||
outlier_strategy: str
|
outlier_strategy: str
|
||||||
outlier_config: OutlierConfig
|
outlier_config: OutlierConfig
|
||||||
@@ -90,6 +93,9 @@ class AppConfig:
|
|||||||
data_path=str(raw["data_path"]),
|
data_path=str(raw["data_path"]),
|
||||||
split_ratios=list(raw["split_ratios"]),
|
split_ratios=list(raw["split_ratios"]),
|
||||||
random_seed=int(raw["random_seed"]),
|
random_seed=int(raw["random_seed"]),
|
||||||
|
split_mode=str(raw.get("split_mode", "grouped_stratified")),
|
||||||
|
split_stratify_target=str(raw.get("split_stratify_target", "V_pi")),
|
||||||
|
split_stratify_bins=int(raw.get("split_stratify_bins", 10)),
|
||||||
remove_duplicate_rows=bool(raw["remove_duplicate_rows"]),
|
remove_duplicate_rows=bool(raw["remove_duplicate_rows"]),
|
||||||
outlier_strategy=str(raw.get("outlier_strategy", "none")),
|
outlier_strategy=str(raw.get("outlier_strategy", "none")),
|
||||||
outlier_config=OutlierConfig(
|
outlier_config=OutlierConfig(
|
||||||
@@ -153,6 +159,12 @@ def load_config(path: str | Path) -> AppConfig:
|
|||||||
raise ValueError("split_ratios 必须为长度为 3 的列表 [train, val, test]")
|
raise ValueError("split_ratios 必须为长度为 3 的列表 [train, val, test]")
|
||||||
if abs(sum(sr) - 1.0) > 1e-6:
|
if abs(sum(sr) - 1.0) > 1e-6:
|
||||||
raise ValueError(f"split_ratios 之和必须为 1,当前为 {sum(sr)}")
|
raise ValueError(f"split_ratios 之和必须为 1,当前为 {sum(sr)}")
|
||||||
|
if cfg.split_mode not in ("random", "grouped_stratified"):
|
||||||
|
raise ValueError("split_mode 必须为 random 或 grouped_stratified")
|
||||||
|
if cfg.split_stratify_target not in ("BW_3dB", "IL", "V_pi"):
|
||||||
|
raise ValueError("split_stratify_target 必须为 BW_3dB、IL 或 V_pi")
|
||||||
|
if cfg.split_stratify_bins < 2:
|
||||||
|
raise ValueError("split_stratify_bins 必须 >= 2")
|
||||||
if cfg.outlier_strategy not in ("none", "iqr", "zscore", "quantile_clip"):
|
if cfg.outlier_strategy not in ("none", "iqr", "zscore", "quantile_clip"):
|
||||||
raise ValueError(f"未知 outlier_strategy: {cfg.outlier_strategy}")
|
raise ValueError(f"未知 outlier_strategy: {cfg.outlier_strategy}")
|
||||||
if cfg.outlier_apply_to not in ("targets", "all"):
|
if cfg.outlier_apply_to not in ("targets", "all"):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import torch
|
import torch
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
from sklearn.preprocessing import StandardScaler
|
from sklearn.preprocessing import StandardScaler
|
||||||
from torch.utils.data import DataLoader, TensorDataset
|
from torch.utils.data import DataLoader, TensorDataset
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ def clean_dataframe(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def stratified_split_indices(
|
def _random_split_indices(
|
||||||
n: int,
|
n: int,
|
||||||
ratios: List[float],
|
ratios: List[float],
|
||||||
seed: int,
|
seed: int,
|
||||||
@@ -143,6 +144,130 @@ def stratified_split_indices(
|
|||||||
return i_train, i_val, i_test
|
return i_train, i_val, i_test
|
||||||
|
|
||||||
|
|
||||||
|
def _quantile_bin_labels(values: np.ndarray, n_bins: int) -> np.ndarray | None:
|
||||||
|
"""
|
||||||
|
基于秩做近似等频分桶,避免重复值导致的 qcut 退化。
|
||||||
|
返回每个样本所属桶标签;若样本过少则返回 None。
|
||||||
|
"""
|
||||||
|
if len(values) < 2:
|
||||||
|
return None
|
||||||
|
q = min(int(n_bins), len(values))
|
||||||
|
if q < 2:
|
||||||
|
return None
|
||||||
|
ranks = pd.Series(values).rank(method="first")
|
||||||
|
labels = pd.qcut(ranks, q=q, labels=False, duplicates="drop")
|
||||||
|
if labels is None:
|
||||||
|
return None
|
||||||
|
arr = np.asarray(labels, dtype=int)
|
||||||
|
if len(np.unique(arr)) < 2:
|
||||||
|
return None
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
def _grouped_split_indices(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
ratios: List[float],
|
||||||
|
seed: int,
|
||||||
|
stratify_target: str,
|
||||||
|
stratify_bins: int,
|
||||||
|
report_lines: List[str],
|
||||||
|
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
|
"""
|
||||||
|
先按输入 8 维分组,再在组级别按目标统计量近似分层切分。
|
||||||
|
这样可避免“同输入异输出”同时落在 train/val/test,提升评估稳定性。
|
||||||
|
若分层条件不足,则退化为组级随机切分。
|
||||||
|
"""
|
||||||
|
if len(df) < 3:
|
||||||
|
raise ValueError("样本数过少,无法做 train/val/test 切分。")
|
||||||
|
|
||||||
|
group_ids = df.groupby(INPUT_COLUMNS, sort=False, dropna=False).ngroup().to_numpy()
|
||||||
|
n_groups = int(group_ids.max()) + 1
|
||||||
|
group_df = df.copy()
|
||||||
|
group_df["_group_id"] = group_ids
|
||||||
|
group_stat = (
|
||||||
|
group_df.groupby("_group_id", sort=True)
|
||||||
|
.agg(group_size=("V_pi", "size"), strat_value=(stratify_target, "median"))
|
||||||
|
.reset_index()
|
||||||
|
)
|
||||||
|
group_id_arr = group_stat["_group_id"].to_numpy(dtype=int)
|
||||||
|
labels = _quantile_bin_labels(
|
||||||
|
group_stat["strat_value"].to_numpy(dtype=np.float64),
|
||||||
|
stratify_bins,
|
||||||
|
)
|
||||||
|
|
||||||
|
tr, va, te = ratios
|
||||||
|
holdout_ratio = va + te
|
||||||
|
val_ratio_in_holdout = va / holdout_ratio
|
||||||
|
|
||||||
|
def _split_groups(use_stratify: bool) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
|
strat = labels if use_stratify and labels is not None else None
|
||||||
|
train_groups, holdout_groups = train_test_split(
|
||||||
|
group_id_arr,
|
||||||
|
train_size=tr,
|
||||||
|
test_size=holdout_ratio,
|
||||||
|
random_state=seed,
|
||||||
|
shuffle=True,
|
||||||
|
stratify=strat,
|
||||||
|
)
|
||||||
|
holdout_strat = None
|
||||||
|
if strat is not None:
|
||||||
|
label_map = dict(zip(group_id_arr.tolist(), labels.tolist()))
|
||||||
|
holdout_labels = np.asarray(
|
||||||
|
[label_map[int(g)] for g in holdout_groups],
|
||||||
|
dtype=int,
|
||||||
|
)
|
||||||
|
if len(np.unique(holdout_labels)) >= 2:
|
||||||
|
holdout_strat = holdout_labels
|
||||||
|
val_groups, test_groups = train_test_split(
|
||||||
|
holdout_groups,
|
||||||
|
train_size=val_ratio_in_holdout,
|
||||||
|
test_size=1.0 - val_ratio_in_holdout,
|
||||||
|
random_state=seed + 1,
|
||||||
|
shuffle=True,
|
||||||
|
stratify=holdout_strat,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
np.asarray(train_groups, dtype=int),
|
||||||
|
np.asarray(val_groups, dtype=int),
|
||||||
|
np.asarray(test_groups, dtype=int),
|
||||||
|
)
|
||||||
|
|
||||||
|
split_note = (
|
||||||
|
f"按输入分组切分,共 {n_groups} 个唯一输入组;"
|
||||||
|
f"组级按 {stratify_target} 中位数分 {min(stratify_bins, n_groups)} 桶近似分层。"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
train_groups, val_groups, test_groups = _split_groups(use_stratify=True)
|
||||||
|
report_lines.append(split_note)
|
||||||
|
except ValueError as e:
|
||||||
|
train_groups, val_groups, test_groups = _split_groups(use_stratify=False)
|
||||||
|
report_lines.append(f"{split_note} 但分层条件不足,退化为组级随机切分:{e}")
|
||||||
|
|
||||||
|
i_train = np.flatnonzero(np.isin(group_ids, train_groups))
|
||||||
|
i_val = np.flatnonzero(np.isin(group_ids, val_groups))
|
||||||
|
i_test = np.flatnonzero(np.isin(group_ids, test_groups))
|
||||||
|
return i_train, i_val, i_test
|
||||||
|
|
||||||
|
|
||||||
|
def build_split_indices(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
cfg: AppConfig,
|
||||||
|
report_lines: List[str],
|
||||||
|
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
|
"""根据配置生成 train/val/test 行索引。"""
|
||||||
|
if cfg.split_mode == "random":
|
||||||
|
report_lines.append("切分策略:随机打乱后按比例切分。")
|
||||||
|
return _random_split_indices(len(df), cfg.split_ratios, cfg.random_seed)
|
||||||
|
return _grouped_split_indices(
|
||||||
|
df=df,
|
||||||
|
ratios=cfg.split_ratios,
|
||||||
|
seed=cfg.random_seed,
|
||||||
|
stratify_target=cfg.split_stratify_target,
|
||||||
|
stratify_bins=cfg.split_stratify_bins,
|
||||||
|
report_lines=report_lines,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_train_only_outliers(
|
def apply_train_only_outliers(
|
||||||
X_train: np.ndarray,
|
X_train: np.ndarray,
|
||||||
y_train: np.ndarray,
|
y_train: np.ndarray,
|
||||||
@@ -404,7 +529,7 @@ def prepare_training_data(
|
|||||||
X = cleaned[INPUT_COLUMNS].to_numpy(dtype=np.float64)
|
X = cleaned[INPUT_COLUMNS].to_numpy(dtype=np.float64)
|
||||||
y = cleaned[TARGET_COLUMNS].to_numpy(dtype=np.float64)
|
y = cleaned[TARGET_COLUMNS].to_numpy(dtype=np.float64)
|
||||||
|
|
||||||
i_tr, i_va, i_te = stratified_split_indices(len(X), cfg.split_ratios, cfg.random_seed)
|
i_tr, i_va, i_te = build_split_indices(cleaned, cfg, report_lines)
|
||||||
save_split_indices(run_dir, i_tr, i_va, i_te)
|
save_split_indices(run_dir, i_tr, i_va, i_te)
|
||||||
X_train, y_train = X[i_tr], y[i_tr]
|
X_train, y_train = X[i_tr], y[i_tr]
|
||||||
X_val, y_val = X[i_va], y[i_va]
|
X_val, y_val = X[i_va], y[i_va]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import torch
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from src.config import load_config
|
from src.config import load_config
|
||||||
|
from src.data import INPUT_COLUMNS
|
||||||
from src.data import load_raw_txt
|
from src.data import load_raw_txt
|
||||||
from src.model import MLPRegressor
|
from src.model import MLPRegressor
|
||||||
from src.preprocess import prepare_training_data
|
from src.preprocess import prepare_training_data
|
||||||
@@ -106,3 +107,87 @@ def test_one_epoch_training_pipeline(tmp_path: Path) -> None:
|
|||||||
assert (run_dir / "checkpoints" / "best.pt").is_file()
|
assert (run_dir / "checkpoints" / "best.pt").is_file()
|
||||||
meta = json.loads((run_dir / "cleaning_meta.json").read_text(encoding="utf-8"))
|
meta = json.loads((run_dir / "cleaning_meta.json").read_text(encoding="utf-8"))
|
||||||
assert meta["n_train"] > 0
|
assert meta["n_train"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_grouped_split_keeps_same_inputs_together(tmp_path: Path) -> None:
|
||||||
|
data_txt = tmp_path / "grouped_data.txt"
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
rows = []
|
||||||
|
base_inputs = rng.normal(size=(24, 8))
|
||||||
|
for x in base_inputs:
|
||||||
|
for _ in range(3):
|
||||||
|
y0 = float(x[0] * 2.0 + rng.normal(scale=0.01))
|
||||||
|
y1 = float(x[1] * -1.5 + rng.normal(scale=0.01))
|
||||||
|
y2 = float(abs(x[2]) * 20.0 + 10.0 + rng.normal(scale=0.1))
|
||||||
|
rows.append(np.concatenate([x, [y0, y1, y2]]))
|
||||||
|
mat = np.asarray(rows, dtype=float)
|
||||||
|
data_txt.write_text(
|
||||||
|
"\n".join(",".join(str(v) for v in row) for row in mat),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg_dict = {
|
||||||
|
"data_path": str(data_txt),
|
||||||
|
"split_ratios": [0.7, 0.15, 0.15],
|
||||||
|
"random_seed": 3,
|
||||||
|
"split_mode": "grouped_stratified",
|
||||||
|
"split_stratify_target": "V_pi",
|
||||||
|
"split_stratify_bins": 6,
|
||||||
|
"remove_duplicate_rows": False,
|
||||||
|
"outlier_strategy": "none",
|
||||||
|
"outlier_apply_to": "targets",
|
||||||
|
"outlier_config": {
|
||||||
|
"iqr_k": 1.5,
|
||||||
|
"zscore_threshold": 4.0,
|
||||||
|
"quantile_lower": 0.001,
|
||||||
|
"quantile_upper": 0.999,
|
||||||
|
},
|
||||||
|
"remove_nonpositive_vpi": False,
|
||||||
|
"filter_v_pi_range": True,
|
||||||
|
"v_pi_min": 0.0,
|
||||||
|
"v_pi_max": 500.0,
|
||||||
|
"model": {
|
||||||
|
"input_dim": 8,
|
||||||
|
"hidden_dims": [16, 16],
|
||||||
|
"output_dim": 3,
|
||||||
|
"batchnorm": False,
|
||||||
|
"dropout": 0.0,
|
||||||
|
"residual": False,
|
||||||
|
},
|
||||||
|
"optimizer": {"name": "adamw", "lr": 0.01, "weight_decay": 0.0},
|
||||||
|
"scheduler": {
|
||||||
|
"type": "cosine",
|
||||||
|
"plateau_factor": 0.5,
|
||||||
|
"plateau_patience": 10,
|
||||||
|
"plateau_min_lr": 1e-6,
|
||||||
|
},
|
||||||
|
"training": {
|
||||||
|
"batch_size": 16,
|
||||||
|
"epochs": 1,
|
||||||
|
"early_stopping_patience": 1,
|
||||||
|
"num_workers": 0,
|
||||||
|
},
|
||||||
|
"loss": {"type": "huber", "huber_delta": 1.0, "target_weights": [1.0, 1.0, 1.0]},
|
||||||
|
"output_dir": str(tmp_path / "results"),
|
||||||
|
}
|
||||||
|
cfg_path = tmp_path / "cfg_grouped.yaml"
|
||||||
|
cfg_path.write_text(yaml.safe_dump(cfg_dict), encoding="utf-8")
|
||||||
|
|
||||||
|
cfg = load_config(cfg_path)
|
||||||
|
df = load_raw_txt(data_txt)
|
||||||
|
run_dir = tmp_path / "run_grouped"
|
||||||
|
run_dir.mkdir()
|
||||||
|
bundle = prepare_training_data(df, cfg, run_dir)
|
||||||
|
|
||||||
|
split_data = json.loads((run_dir / "split_indices.json").read_text(encoding="utf-8"))
|
||||||
|
split_name_by_row = {}
|
||||||
|
for split_name, indices in split_data.items():
|
||||||
|
for idx in indices:
|
||||||
|
split_name_by_row[int(idx)] = split_name
|
||||||
|
|
||||||
|
cleaned = df.reset_index(drop=True)
|
||||||
|
for _, sub in cleaned.groupby(INPUT_COLUMNS, dropna=False):
|
||||||
|
assigned = {split_name_by_row[int(i)] for i in sub.index.to_list()}
|
||||||
|
assert len(assigned) == 1
|
||||||
|
|
||||||
|
assert len(bundle.X_train) > 0
|
||||||
|
|||||||
Reference in New Issue
Block a user