modified: .DS_Store
modified: checkpoints_final/task1_final/task1_final_model.pkl new file: checkpoints_final/task1_final/task1_tmd_adapter.pkl new file: scripts_4/README.md new file: scripts_4/__init__.py new file: scripts_4/__pycache__/adapter.cpython-314.pyc new file: scripts_4/adapter.py new file: scripts_4/predict_tmd_single.py new file: scripts_4/train_tmd_adapter.py
This commit is contained in:
Binary file not shown.
BIN
checkpoints_final/task1_final/task1_tmd_adapter.pkl
Normal file
BIN
checkpoints_final/task1_final/task1_tmd_adapter.pkl
Normal file
Binary file not shown.
45
scripts_4/README.md
Normal file
45
scripts_4/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# scripts_4: TMD 小样本适配
|
||||||
|
|
||||||
|
本目录实现二阶段方案:
|
||||||
|
|
||||||
|
1. 基础模型(`Non_TMD` 训练得到的 `task1_final_model.pkl`)先给出基线预测;
|
||||||
|
2. 用极少量 `TMD` 样本拟合频率相关比例系数 `k(f)`;
|
||||||
|
3. 输出 `TMD` 预测:`y_tmd = k(f) * y_base`。
|
||||||
|
|
||||||
|
## 为什么这样做
|
||||||
|
|
||||||
|
- 仅 5 个 TMD 点直接重训主模型容易过拟合;
|
||||||
|
- 保留基础模型泛化能力,只学习域偏移;
|
||||||
|
- 通过 `shrinkage` 把校正系数向 `1.0` 收缩,降低小样本波动影响;
|
||||||
|
- 频率外推时会自动向 `1.0` 回归,避免越界夸张。
|
||||||
|
|
||||||
|
## 脚本说明
|
||||||
|
|
||||||
|
- `train_tmd_adapter.py`:拟合并保存适配器,同时输出 LOOCV 报告。
|
||||||
|
- `predict_tmd_single.py`:加载基础模型 + 适配器,对单个 TMD 文件推理。
|
||||||
|
- `adapter.py`:频率校正器定义与序列化工具。
|
||||||
|
|
||||||
|
## 使用方法(请使用虚拟环境 Python)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python scripts_4/train_tmd_adapter.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python scripts_4/predict_tmd_single.py \
|
||||||
|
--file downloads/TMD/val/harmonic_5mm_1.25Hz_TMD.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
可选参数示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python scripts_4/train_tmd_adapter.py \
|
||||||
|
--shrinkage 0.25 \
|
||||||
|
--extrapolation-decay-hz 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输出文件
|
||||||
|
|
||||||
|
- 适配器:`checkpoints_final/task1_final/task1_tmd_adapter.pkl`
|
||||||
|
- 报告:`evaluation_outputs/task1_final/tmd_adapter_report.json`
|
||||||
|
|
||||||
2
scripts_4/__init__.py
Normal file
2
scripts_4/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Task4: Tiny-sample TMD adaptation on top of baseline RMS model."""
|
||||||
|
|
||||||
BIN
scripts_4/__pycache__/adapter.cpython-314.pyc
Normal file
BIN
scripts_4/__pycache__/adapter.cpython-314.pyc
Normal file
Binary file not shown.
101
scripts_4/adapter.py
Normal file
101
scripts_4/adapter.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(value: float) -> float:
|
||||||
|
return float(np.asarray(value, dtype=np.float64).reshape(-1)[0])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FrequencyScaleAdapter:
|
||||||
|
frequencies_hz: np.ndarray
|
||||||
|
scale_values: np.ndarray
|
||||||
|
shrinkage: float = 0.2
|
||||||
|
extrapolation_decay_hz: float = 0.25
|
||||||
|
min_scale: float = 0.4
|
||||||
|
max_scale: float = 2.5
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
freqs = np.asarray(self.frequencies_hz, dtype=np.float64).reshape(-1)
|
||||||
|
scales = np.asarray(self.scale_values, dtype=np.float64).reshape(-1)
|
||||||
|
if freqs.size == 0:
|
||||||
|
raise ValueError("frequencies_hz cannot be empty.")
|
||||||
|
if freqs.size != scales.size:
|
||||||
|
raise ValueError("frequencies_hz and scale_values must have the same length.")
|
||||||
|
order = np.argsort(freqs)
|
||||||
|
freqs = freqs[order]
|
||||||
|
scales = np.clip(scales[order], self.min_scale, self.max_scale)
|
||||||
|
# Pull scales toward 1.0 to reduce overfitting with tiny TMD samples.
|
||||||
|
scales = 1.0 + (scales - 1.0) * (1.0 - float(np.clip(self.shrinkage, 0.0, 0.95)))
|
||||||
|
self.frequencies_hz = freqs
|
||||||
|
self.scale_values = scales
|
||||||
|
|
||||||
|
def _inside_range_weight(self, frequency_hz: float) -> float:
|
||||||
|
left = self.frequencies_hz[0]
|
||||||
|
right = self.frequencies_hz[-1]
|
||||||
|
f = _safe_float(frequency_hz)
|
||||||
|
if left <= f <= right:
|
||||||
|
return 1.0
|
||||||
|
distance = min(abs(f - left), abs(f - right))
|
||||||
|
decay = max(_safe_float(self.extrapolation_decay_hz), 1e-6)
|
||||||
|
return float(np.exp(-distance / decay))
|
||||||
|
|
||||||
|
def scale_at(self, frequency_hz: float) -> float:
|
||||||
|
f = _safe_float(frequency_hz)
|
||||||
|
boundary_interp = float(np.interp(f, self.frequencies_hz, self.scale_values))
|
||||||
|
in_range_weight = self._inside_range_weight(f)
|
||||||
|
scale = 1.0 + in_range_weight * (boundary_interp - 1.0)
|
||||||
|
return float(np.clip(scale, self.min_scale, self.max_scale))
|
||||||
|
|
||||||
|
def predict(self, base_rms: float, frequency_hz: float) -> float:
|
||||||
|
return max(_safe_float(base_rms), 0.0) * self.scale_at(frequency_hz)
|
||||||
|
|
||||||
|
def to_payload(self) -> dict:
|
||||||
|
return {
|
||||||
|
"frequencies_hz": self.frequencies_hz.tolist(),
|
||||||
|
"scale_values": self.scale_values.tolist(),
|
||||||
|
# scale_values are already post-shrinkage values after __post_init__.
|
||||||
|
"shrinkage": 0.0,
|
||||||
|
"extrapolation_decay_hz": float(self.extrapolation_decay_hz),
|
||||||
|
"min_scale": float(self.min_scale),
|
||||||
|
"max_scale": float(self.max_scale),
|
||||||
|
"already_shrunk": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_payload(cls, payload: dict) -> "FrequencyScaleAdapter":
|
||||||
|
shrinkage = float(payload.get("shrinkage", 0.2))
|
||||||
|
if bool(payload.get("already_shrunk", False)):
|
||||||
|
shrinkage = 0.0
|
||||||
|
return cls(
|
||||||
|
frequencies_hz=np.asarray(payload["frequencies_hz"], dtype=np.float64),
|
||||||
|
scale_values=np.asarray(payload["scale_values"], dtype=np.float64),
|
||||||
|
shrinkage=shrinkage,
|
||||||
|
extrapolation_decay_hz=float(payload.get("extrapolation_decay_hz", 0.25)),
|
||||||
|
min_scale=float(payload.get("min_scale", 0.4)),
|
||||||
|
max_scale=float(payload.get("max_scale", 2.5)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_adapter(adapter: FrequencyScaleAdapter, output_path: Path, extra: dict | None = None) -> None:
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
"adapter_type": "frequency_scale_interp_v1",
|
||||||
|
"adapter": adapter.to_payload(),
|
||||||
|
"extra": extra or {},
|
||||||
|
}
|
||||||
|
with output_path.open("wb") as handle:
|
||||||
|
pickle.dump(payload, handle)
|
||||||
|
|
||||||
|
|
||||||
|
def load_adapter(adapter_path: Path) -> tuple[FrequencyScaleAdapter, dict]:
|
||||||
|
with adapter_path.open("rb") as handle:
|
||||||
|
payload = pickle.load(handle)
|
||||||
|
adapter = FrequencyScaleAdapter.from_payload(payload["adapter"])
|
||||||
|
extra = payload.get("extra", {})
|
||||||
|
return adapter, extra
|
||||||
78
scripts_4/predict_tmd_single.py
Normal file
78
scripts_4/predict_tmd_single.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import pickle
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .adapter import load_adapter
|
||||||
|
except ImportError:
|
||||||
|
from adapter import load_adapter
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module(module_name: str, file_path: Path):
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise ImportError(f"Cannot load module from {file_path}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[module_name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Predict TMD RMS using baseline model + frequency adapter.")
|
||||||
|
parser.add_argument("--file", type=str, required=True)
|
||||||
|
parser.add_argument("--checkpoint", type=str, default="checkpoints_final/task1_final/task1_final_model.pkl")
|
||||||
|
parser.add_argument("--adapter", type=str, default="checkpoints_final/task1_final/task1_tmd_adapter.pkl")
|
||||||
|
parser.add_argument("--data-dir", type=str, default="downloads/TMD")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
config_module = _load_module("config", PROJECT_ROOT / "scripts" / "config.py")
|
||||||
|
dataset_module = _load_module("task1_dataset", PROJECT_ROOT / "scripts" / "dataset.py")
|
||||||
|
make_experiment_config = getattr(config_module, "make_experiment_config")
|
||||||
|
build_record_from_file = getattr(dataset_module, "build_record_from_file")
|
||||||
|
|
||||||
|
config = make_experiment_config()
|
||||||
|
config.data.data_dir = args.data_dir
|
||||||
|
config.data.__post_init__()
|
||||||
|
|
||||||
|
ckpt_path = (PROJECT_ROOT / args.checkpoint).resolve()
|
||||||
|
adapter_path = (PROJECT_ROOT / args.adapter).resolve()
|
||||||
|
with ckpt_path.open("rb") as handle:
|
||||||
|
payload = pickle.load(handle)
|
||||||
|
model = payload["model"]
|
||||||
|
adapter, extra = load_adapter(adapter_path)
|
||||||
|
|
||||||
|
file_path = Path(args.file).resolve()
|
||||||
|
record = build_record_from_file(file_path, config.data)
|
||||||
|
|
||||||
|
pred_tr = max(float(model.predict([record.features.tolist()])[0]), config.data.normalization_eps)
|
||||||
|
pred_base = pred_tr * record.x_rms
|
||||||
|
pred_tmd = adapter.predict(pred_base, record.frequency_hz)
|
||||||
|
|
||||||
|
true_rms = float(record.y_rms)
|
||||||
|
base_err = abs(pred_base - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
||||||
|
tmd_err = abs(pred_tmd - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
||||||
|
|
||||||
|
print(f"Checkpoint: {ckpt_path}")
|
||||||
|
print(f"Adapter: {adapter_path}")
|
||||||
|
print(f"Adapter extra: {extra}")
|
||||||
|
print(f"Input file: {file_path}")
|
||||||
|
print(f"Frequency (Hz): {record.frequency_hz:.6f}")
|
||||||
|
print(f"x_rms: {record.x_rms:.6f}")
|
||||||
|
print(f"True y_rms: {true_rms:.6f}")
|
||||||
|
print(f"Base pred y_rms: {pred_base:.6f} | error={base_err:.4f}%")
|
||||||
|
print(f"Adapted pred y_rms: {pred_tmd:.6f} | error={tmd_err:.4f}%")
|
||||||
|
print(f"Applied scale k(f): {adapter.scale_at(record.frequency_hz):.6f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
164
scripts_4/train_tmd_adapter.py
Normal file
164
scripts_4/train_tmd_adapter.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import pickle
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts"))
|
||||||
|
|
||||||
|
from config import make_experiment_config
|
||||||
|
from dataset import build_record_from_file
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .adapter import FrequencyScaleAdapter, save_adapter
|
||||||
|
except ImportError:
|
||||||
|
from adapter import FrequencyScaleAdapter, save_adapter
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Fit a tiny-sample TMD adapter on top of the Non_TMD baseline .pkl model.")
|
||||||
|
parser.add_argument("--checkpoint", type=str, default=None, help="Path to baseline model .pkl.")
|
||||||
|
parser.add_argument("--tmd-dir", type=str, default="downloads/TMD", help="Directory that contains TMD harmonic csv files.")
|
||||||
|
parser.add_argument("--output", type=str, default="checkpoints_final/task1_final/task1_tmd_adapter.pkl")
|
||||||
|
parser.add_argument("--report-json", type=str, default="evaluation_outputs/task1_final/tmd_adapter_report.json")
|
||||||
|
parser.add_argument("--shrinkage", type=float, default=0.20, help="Scale shrinkage toward 1.0.")
|
||||||
|
parser.add_argument("--extrapolation-decay-hz", type=float, default=0.25)
|
||||||
|
parser.add_argument("--min-scale", type=float, default=0.40)
|
||||||
|
parser.add_argument("--max-scale", type=float, default=2.50)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_checkpoint_path(checkpoint_arg: str | None) -> Path:
|
||||||
|
if checkpoint_arg:
|
||||||
|
return Path(checkpoint_arg).resolve()
|
||||||
|
return PROJECT_ROOT / "checkpoints_final" / "task1_final" / "task1_final_model.pkl"
|
||||||
|
|
||||||
|
|
||||||
|
def load_baseline_model(model_path: Path):
|
||||||
|
with model_path.open("rb") as handle:
|
||||||
|
payload = pickle.load(handle)
|
||||||
|
return payload["model"], payload.get("summary", {})
|
||||||
|
|
||||||
|
|
||||||
|
def base_predict_rms(model, features: np.ndarray, x_rms: float, eps: float) -> float:
|
||||||
|
pred_tr = max(float(model.predict([features.tolist()])[0]), eps)
|
||||||
|
return pred_tr * float(x_rms)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_tmd_rows(model, tmd_dir: Path, eps: float) -> list[dict]:
|
||||||
|
config = make_experiment_config().data
|
||||||
|
config.data_dir = str(Path(tmd_dir).relative_to(PROJECT_ROOT))
|
||||||
|
config.__post_init__()
|
||||||
|
rows: list[dict] = []
|
||||||
|
for file_path in sorted(config.data_root.rglob("harmonic_5mm_*Hz_TMD.csv")):
|
||||||
|
record = build_record_from_file(file_path, config)
|
||||||
|
base_pred = base_predict_rms(model, record.features, record.x_rms, eps)
|
||||||
|
scale = float(record.y_rms / max(base_pred, eps))
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"file_name": file_path.name,
|
||||||
|
"frequency_hz": float(record.frequency_hz),
|
||||||
|
"true_y_rms": float(record.y_rms),
|
||||||
|
"base_pred_y_rms": float(base_pred),
|
||||||
|
"scale_true_over_base": float(scale),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
raise RuntimeError("No TMD harmonic files found.")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def loocv_error(rows: list[dict], shrinkage: float, extrapolation_decay_hz: float, min_scale: float, max_scale: float) -> list[float]:
|
||||||
|
errors: list[float] = []
|
||||||
|
eps = 1e-12
|
||||||
|
for holdout in range(len(rows)):
|
||||||
|
train_rows = [row for i, row in enumerate(rows) if i != holdout]
|
||||||
|
adapter = FrequencyScaleAdapter(
|
||||||
|
frequencies_hz=np.asarray([row["frequency_hz"] for row in train_rows], dtype=np.float64),
|
||||||
|
scale_values=np.asarray([row["scale_true_over_base"] for row in train_rows], dtype=np.float64),
|
||||||
|
shrinkage=shrinkage,
|
||||||
|
extrapolation_decay_hz=extrapolation_decay_hz,
|
||||||
|
min_scale=min_scale,
|
||||||
|
max_scale=max_scale,
|
||||||
|
)
|
||||||
|
sample = rows[holdout]
|
||||||
|
pred = adapter.predict(sample["base_pred_y_rms"], sample["frequency_hz"])
|
||||||
|
err = abs(pred - sample["true_y_rms"]) / max(abs(sample["true_y_rms"]), eps) * 100.0
|
||||||
|
errors.append(float(err))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
ckpt_path = resolve_checkpoint_path(args.checkpoint)
|
||||||
|
model, baseline_summary = load_baseline_model(ckpt_path)
|
||||||
|
rows = collect_tmd_rows(model, (PROJECT_ROOT / args.tmd_dir).resolve(), eps=1e-6)
|
||||||
|
rows = sorted(rows, key=lambda x: x["frequency_hz"])
|
||||||
|
|
||||||
|
adapter = FrequencyScaleAdapter(
|
||||||
|
frequencies_hz=np.asarray([row["frequency_hz"] for row in rows], dtype=np.float64),
|
||||||
|
scale_values=np.asarray([row["scale_true_over_base"] for row in rows], dtype=np.float64),
|
||||||
|
shrinkage=args.shrinkage,
|
||||||
|
extrapolation_decay_hz=args.extrapolation_decay_hz,
|
||||||
|
min_scale=args.min_scale,
|
||||||
|
max_scale=args.max_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
adapted_rows = []
|
||||||
|
for row in rows:
|
||||||
|
adapted_pred = adapter.predict(row["base_pred_y_rms"], row["frequency_hz"])
|
||||||
|
adapted_err = abs(adapted_pred - row["true_y_rms"]) / max(abs(row["true_y_rms"]), 1e-12) * 100.0
|
||||||
|
adapted_rows.append({**row, "adapted_pred_y_rms": adapted_pred, "adapted_err_pct": adapted_err})
|
||||||
|
|
||||||
|
loocv_errors = loocv_error(
|
||||||
|
rows=rows,
|
||||||
|
shrinkage=args.shrinkage,
|
||||||
|
extrapolation_decay_hz=args.extrapolation_decay_hz,
|
||||||
|
min_scale=args.min_scale,
|
||||||
|
max_scale=args.max_scale,
|
||||||
|
)
|
||||||
|
report = {
|
||||||
|
"checkpoint": str(ckpt_path),
|
||||||
|
"baseline_summary": baseline_summary,
|
||||||
|
"samples": adapted_rows,
|
||||||
|
"adapter_scales_after_shrinkage": [
|
||||||
|
{"frequency_hz": float(f), "scale": float(s)}
|
||||||
|
for f, s in zip(adapter.frequencies_hz.tolist(), adapter.scale_values.tolist())
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"base_mean_err_pct": float(
|
||||||
|
np.mean(
|
||||||
|
[
|
||||||
|
abs(row["base_pred_y_rms"] - row["true_y_rms"]) / max(abs(row["true_y_rms"]), 1e-12) * 100.0
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"adapted_mean_err_pct": float(np.mean([row["adapted_err_pct"] for row in adapted_rows])),
|
||||||
|
"loocv_mean_err_pct": float(np.mean(loocv_errors)),
|
||||||
|
"loocv_max_err_pct": float(np.max(loocv_errors)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output_path = (PROJECT_ROOT / args.output).resolve()
|
||||||
|
save_adapter(adapter, output_path, extra={"report_metrics": report["metrics"], "checkpoint": str(ckpt_path)})
|
||||||
|
|
||||||
|
report_path = (PROJECT_ROOT / args.report_json).resolve()
|
||||||
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
print(f"Adapter saved to: {output_path}")
|
||||||
|
print(f"Report saved to: {report_path}")
|
||||||
|
print(pd.DataFrame(adapted_rows).to_string(index=False))
|
||||||
|
print(f"LOOCV mean error (%): {report['metrics']['loocv_mean_err_pct']:.4f}")
|
||||||
|
print(f"LOOCV max error (%): {report['metrics']['loocv_max_err_pct']:.4f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user