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
57 changes: 56 additions & 1 deletion REPRODUCIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## リポジトリに含まれる範囲(目安)

- **含む:** `tda_ml/`、**`configs/` 直下の正本 YAML**(`base.yaml` と `reproduce` / `dev` / `prod` / `test_fast` の各ファイル)、`tests/`、追跡されている `scripts/`、`experiments/run_backend_multiseed.py`、および `README.md` / `REPRODUCIBILITY.md` / `pyproject.toml` / `uv.lock` / `LICENSE` / `CITATION.cff` などのメタデータ。
- **含む:** `tda_ml/`、**`configs/` 直下の正本 YAML**(`base.yaml` と `reproduce` / `dev` / `prod` / `test_fast` の各ファイル)、`tests/`、追跡されている `scripts/`、`experiments/run_backend_multiseed.py`、`experiments/issue59_verify_mahalanobis.py`、および `README.md` / `REPRODUCIBILITY.md` / `pyproject.toml` / `uv.lock` / `LICENSE` / `CITATION.cff` などのメタデータ。
- **含めない:** `docs/` 以下、`configs/archive/`(履歴用 YAML を置く場合は **ローカルのみ**)、`outputs/`、`data/` など。`load_config("archive/...")` は、手元に `configs/archive/*.yaml` を置いた場合にのみ使えます。

## 環境
Expand Down Expand Up @@ -58,6 +58,61 @@ uv run python experiments/run_backend_multiseed.py \

したがって、`run_backend_multiseed.py` で同じ YAML を回しても、**位相損失が見ている距離空間はバックエンド間で同一ではありません**。ここでは「同一のデータ・スケジュール・設定表面での再現パイプライン比較」を意図しており、**両バックエンドが数学的に完全に同型の重み付き距離目的関数を共有する**という読み方はしません。`ellphi` 側に Mahalanobis の確率重みに相当する項を無理に足す予定はなく、比較の解釈は本節および `README.md` の英語節(*Backend comparison: outlier-probability weighting*)に従ってください。

## 教師あり学習の目的関数(論文 Methods 用)

本線 `tda_ml/` の学習は **点ラベル BCE** と **clean 点群の $H_1$ 持久図との Wasserstein 教師** を併用します(`configs/reproduce.yaml` 系)。

\[
\mathcal{L}
= w_{\mathrm{class}}\mathcal{L}_{\mathrm{BCE}}
+ w_{\mathrm{topo}}\, W_2^2\!\bigl(\mathrm{PD}_{H_1}(D^{\theta,p}),\,\mathrm{PD}_{H_1}(X_{\mathrm{clean}})\bigr)
+ w_{\mathrm{size}}\mathcal{L}_{\mathrm{size}}
+ w_{\mathrm{aniso}}\mathcal{L}_{\mathrm{aniso}}.
\]

**楕円パラメータ(`axis_param=legacy`、主表の既定)** — 局所 PCA の $a_{i,\mathrm{base}}, b_{i,\mathrm{base}}, \theta_{i,\mathrm{base}}$ に対し:

\[
a_i = a_{i,\mathrm{base}}\, e^{\Delta a_i},\quad
b_i = b_{i,\mathrm{base}}\, e^{\Delta b_i},\quad
\theta_i = \theta_{i,\mathrm{base}} + \tanh(\Delta\theta_i)\frac{\pi}{2}.
\]

**正則化(`tda_ml/losses.py`)**

\[
\mathcal{L}_{\mathrm{size}} = \frac{1}{N}\sum_i (M_i^2 + m_i^2),\quad
M_i=\max(a_i,b_i),\; m_i=\min(a_i,b_i).
\]

主表 config では `aniso_mode: linear` により
$\mathcal{L}_{\mathrm{aniso}} = \frac{1}{N}\sum_i R_i$($R_i=M_i/m_i$)。
図用 ablation(`ablation_localscale_try2` 等)では
`aniso_mode: barrier` として
$\mathcal{L}_{\mathrm{aniso}} = \frac{10}{N}\sum_i \mathrm{ReLU}(R_i-\tau)^2$。

**Mahalanobis 距離**(`tda_ml/topology.py`)は outlier 確率 $p_i$ により二乗距離を
$1/\bigl((1-p_i)(1-p_j)\bigr)$ で重み付け(`INLIER_PROB_MIN` で下限クリップ)。

**数値安定化のみの定数**(モデリング床ではない)は `tda_ml/numerical_eps.py` に集約し、付録で列挙します。encoder の `clamp(0.2)` や legacy の `+10^{-4}` といった**論文に無い床は削除済み**(issue #59)。
Comment thread
t-uda marked this conversation as resolved.

### issue #59 検証(早期打ち切り + 診断)

床削除後の教師あり本線を確認するには:

```bash
uv run python experiments/issue59_verify_mahalanobis.py
Comment thread
t-uda marked this conversation as resolved.
```

- 設定: `configs/issue59_verify_mahalanobis.yaml`(`reproduce_1week_tuned` 相当・**mahalanobis**)
- **`probe_epochs` 後**(`configs/issue59_verify_mahalanobis.yaml` では既定 8)も `val_mcc` / `train_mcc` が閾値(0.05 / 0.02)を超えない、または全 outlier 予測(`val_recall≈1`)なら **学習を打ち切り**
- 打ち切り時の成果物(`<run_dir>/logs/`):
- `issue59_abort_report.json` / `.md` — 楕円軸・encoder PCA・距離行列・分類の統計と**原因仮説リスト**
- `abort_checkpoint.pth` — 打ち切り時点の重み
- `run_manifest.json` — コミット SHA・early_abort 設定

CLI: `--epochs 12 --seed 42 --out-base outputs/issue59_verify`

## 図・定性出力

`docs/paper/` 以下の LaTeX の **すべての図を一括で出す単一スクリプトはありません**。原稿専用のアセットもあります。下の表は **コードに近い** 図の流れを示すものです。論文の図を増やしたら表も追記してください。
Expand Down
52 changes: 52 additions & 0 deletions configs/issue59_verify_mahalanobis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Issue #59 verification: reproduce_1week_tuned + mahalanobis + early MCC abort + diagnostics.
# Mahalanobis only; aborts if MCC stays ~0 after probe_epochs (default 8 — size reg needs epochs).

meta:
config_id: "issue59_verify_mahalanobis"

model:
point_dim: 2
feature_dim: 128
threshold: 0.5
topology_loss:
distance_backend: "mahalanobis"
ellphi_differentiable: true

loss:
w_class: 1.0
w_topo: 0.2
w_aniso: 0.01099204345474479
w_size: 0.0005578582308620256
pos_weight: 1.0
aniso_mode: "linear"

training:
lr: 0.0004897466143769238
epochs: 12
grad_clip_value: 1.0
visualize_every: 1
warmup_epochs: 0
use_amp: true
early_abort:
enabled: true
probe_epochs: 8
min_val_mcc: 0.05
min_train_mcc: 0.02
max_val_recall_for_abort: 0.99

data:
max_points: 100
num_outliers: 20
seed: 42
test_size: 1000
num_workers: 4
batch_size: 16
pin_memory: true

performance:
cudnn_benchmark: true
enable_tf32: true
matmul_precision: high

outputs:
base_dir: "outputs"
101 changes: 101 additions & 0 deletions experiments/issue59_verify_mahalanobis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Issue #59 supervised verification (mahalanobis, reproduce_1week_tuned hyperparameters).

Runs training with early abort when val/train MCC stay near zero after probe_epochs.
On abort, writes ``logs/issue59_abort_report.{json,md}`` with ellipse / encoder /
distance / classification diagnostics and ranked failure hypotheses.

Usage::

uv run python experiments/issue59_verify_mahalanobis.py
uv run python experiments/issue59_verify_mahalanobis.py --epochs 12 --seed 42
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

from tda_ml.config import deep_update, load_config
from tda_ml.main import main as train_main
from tda_ml.supervised_diagnostics import git_revision

REPO = Path(__file__).resolve().parents[1]


def preflight(config_name: str) -> dict:
cfg = load_config(config_name, project_root=REPO)
data = cfg.get("data", {})
training = cfg.get("training", {})
early = training.get("early_abort", {})
if not early.get("enabled", False):
raise ValueError(
f"{config_name}: training.early_abort.enabled must be true for verification"
)
backend = (
cfg.get("model", {}).get("topology_loss", {}).get("distance_backend", "")
)
if backend != "mahalanobis":
raise ValueError(
f"issue59 verification expects mahalanobis backend; got {backend!r}"
)
data_root = REPO / "data"
if not data_root.exists():
raise FileNotFoundError(
f"MNIST data root missing: {data_root}. Run training once to download."
)
return {
"config_id": cfg["meta"].get("config_id"),
"source_revision": git_revision(REPO),
"epochs": training.get("epochs"),
"seed": data.get("seed"),
"early_abort": early,
"distance_backend": backend,
}


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--config",
default="issue59_verify_mahalanobis",
help="Config name under configs/ (default: issue59_verify_mahalanobis)",
)
parser.add_argument("--epochs", type=int, default=None)
parser.add_argument("--seed", type=int, default=None)
parser.add_argument(
"--out-base",
type=Path,
default=REPO / "outputs" / "issue59_verify",
)
args = parser.parse_args()

manifest_preview = preflight(args.config)
print("=== Preflight OK ===")
print(json.dumps(manifest_preview, indent=2, ensure_ascii=True))

cfg = load_config(args.config, project_root=REPO)
overrides: dict = {"outputs": {"base_dir": str(args.out_base)}}
if args.epochs is not None:
overrides.setdefault("training", {})["epochs"] = args.epochs
if args.seed is not None:
overrides.setdefault("data", {})["seed"] = args.seed
cfg = deep_update(cfg, overrides)

result = train_main(config=cfg)
status = result.get("status", "completed")
print("\n=== Verification result ===")
print(f"status: {status}")
print(f"run_dir: {result.get('run_dir')}")
print(f"best_val_mcc: {result.get('best_val_mcc', result.get('val_mcc')):.4f}")
if result.get("abort_report"):
print(f"abort_report: {result['abort_report']}")
print("\nReview hypotheses in issue59_abort_report.md before long runs.")
return 2
return 0


if __name__ == "__main__":
raise SystemExit(main())
7 changes: 5 additions & 2 deletions tda_ml/losses.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from torch_topological.nn import VietorisRipsComplex, WassersteinDistance

from tda_ml.distance_backend import compute_distance_matrix_batch
from tda_ml.numerical_eps import NUMERICAL_EPS
from tda_ml.topology import compute_anisotropic_distance_matrix

# Distance-mode aliases for topology loss (kept for config/test compatibility).
Expand Down Expand Up @@ -109,7 +110,7 @@ def forward(self, params):
major_axis = axes.max(dim=-1)[0]
minor_axis = axes.min(dim=-1)[0]

aspect_ratios = major_axis / (minor_axis + 1e-6)
aspect_ratios = major_axis / (minor_axis + NUMERICAL_EPS)

if self.mode == 'barrier':
barrier_term = F.relu(aspect_ratios - self.barrier_threshold).pow(2).mean()
Expand Down Expand Up @@ -193,6 +194,8 @@ def forward(self, points, params, logits, clean_pd_info):
)

if valid_samples == 0:
return torch.tensor(0.0, device=points.device, requires_grad=True)
raise RuntimeError(
"TopologicalLoss: all batch items failed; no valid Wasserstein terms"
)

return self.weight * (total_loss / valid_samples)
Loading
Loading