diff options
| author | haoyuren <13851610112@163.com> | 2026-07-28 21:17:38 +0800 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-07-28 21:17:38 +0800 |
| commit | 3b5f01cb4db9efc4ceb2ba17709bbfc82a63e660 (patch) | |
| tree | ad4e2321373fc9e551ffeb2ca27f5533ca9e9bdb /scripts/generate_paper_tables.py | |
| parent | 18bf25f34f6d352605086aa8fe63977baacc315b (diff) | |
Add appendix table generator and generated LaTeX fragments
generate_paper_tables.py reads the row-level CSVs and emits the four
appendix tables (initialization calibration rows, finite-time cells,
CNN calibration rows, learning-rate regimes) with the paper's nested
uncertainty convention. The fragments are included by the manuscript
via \input; no table value is transcribed manually.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'scripts/generate_paper_tables.py')
| -rw-r--r-- | scripts/generate_paper_tables.py | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/scripts/generate_paper_tables.py b/scripts/generate_paper_tables.py new file mode 100644 index 0000000..ed66dae --- /dev/null +++ b/scripts/generate_paper_tables.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Generate LaTeX table fragments for the paper appendices from row-level CSVs. + +Each fragment contains only a tabular environment; captions and labels live in +the paper source. Uncertainty follows the paper's nesting convention: feedback +draws are averaged within each forward initialization first, and standard +errors are computed across forward-initialization means. +""" + +from __future__ import annotations + +import argparse +import csv +import math +import statistics +from collections import defaultdict +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--depth-dir", type=Path, default=Path("outputs/aaai_depth_experiments")) + parser.add_argument( + "--cnn-dir", type=Path, default=Path("outputs/cnn_initialization_validation") + ) + parser.add_argument( + "--lr-dir", type=Path, default=Path("outputs/learning_rate_compensation") + ) + parser.add_argument("--outdir", type=Path, default=Path("outputs/tables")) + return parser.parse_args() + + +def read_rows(path: Path) -> list[dict[str, str]]: + if not path.exists(): + raise FileNotFoundError(path) + with path.open() as handle: + return list(csv.DictReader(handle)) + + +def sem(values: list[float]) -> float: + if len(values) < 2: + return float("nan") + return statistics.stdev(values) / math.sqrt(len(values)) + + +def write_fragment(path: Path, lines: list[str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines) + "\n") + print(f"wrote {path}") + + +def initialization_table(depth_dir: Path, outdir: Path) -> None: + rows = read_rows(depth_dir / "initialization_rows.csv") + cells: dict[tuple[int, int], dict[str, dict[str, float]]] = defaultdict(dict) + for row in rows: + key = (int(row["depth"]), int(row["init_seed"])) + cells[key][row["rule"]] = { + "pred": float(row["predicted_deficit"]), + "mean": float(row["empirical_deficit"]), + "se": float(row["empirical_stderr"]), + } + lines = [ + r"\begin{tabular}{@{}rrccc@{}}", + r"\toprule", + r"Depth & Init & Prediction & FA measured (SE) & DFA measured (SE) \\", + r"\midrule", + ] + for (depth, init_seed), by_rule in sorted(cells.items()): + fa, dfa = by_rule["FA"], by_rule["DFA"] + assert abs(fa["pred"] - dfa["pred"]) < 1e-12, (depth, init_seed) + lines.append( + f"{depth} & {init_seed - 10_000} & {fa['pred']:.6f} & " + f"{fa['mean']:.6f} ({fa['se']:.2g}) & " + f"{dfa['mean']:.6f} ({dfa['se']:.2g}) \\\\" + ) + lines += [r"\bottomrule", r"\end{tabular}"] + write_fragment(outdir / "initialization_rows.tex", lines) + + +def finite_time_table(depth_dir: Path, outdir: Path) -> None: + rows = read_rows(depth_dir / "finite_time_rows.csv") + cells: dict[tuple[int, int], dict[int, dict[str, list[float]]]] = defaultdict( + lambda: defaultdict(lambda: defaultdict(list)) + ) + fields = ["empirical_gap", "fixed_gap", "velocity_gap", "retangent_gap"] + for row in rows: + cell = cells[(int(row["depth"]), int(row["width"]))][int(row["init_seed"])] + for field in fields: + cell[field].append(float(row[field])) + lines = [ + r"\begin{tabular}{@{}rrccccc@{}}", + r"\toprule", + r"Depth & Width & Measured & SE & Frozen & Linear & Relinearized \\", + r"\midrule", + ] + for (depth, width), by_init in sorted(cells.items()): + init_means = { + field: [statistics.mean(values[field]) for _, values in sorted(by_init.items())] + for field in fields + } + measured = statistics.mean(init_means["empirical_gap"]) + lines.append( + f"{depth} & {width} & {measured:.6f} & {sem(init_means['empirical_gap']):.2g} & " + f"{statistics.mean(init_means['fixed_gap']):.6f} & " + f"{statistics.mean(init_means['velocity_gap']):.6f} & " + f"{statistics.mean(init_means['retangent_gap']):.6f} \\\\" + ) + lines += [r"\bottomrule", r"\end{tabular}"] + write_fragment(outdir / "finite_time_cells.tex", lines) + + +def cnn_table(cnn_dir: Path, outdir: Path) -> None: + rows = read_rows(cnn_dir / "cnn_initialization_rows.csv") + lines = [ + r"\begin{tabular}{@{}lrccc@{}}", + r"\toprule", + r"Rule & Init & Prediction & Measured & SE \\", + r"\midrule", + ] + for row in sorted(rows, key=lambda r: (r["rule"], int(r["init_seed"]))): + lines.append( + f"{row['rule']} & {int(row['init_seed']) - 10_000} & " + f"{float(row['predicted_deficit']):.6f} & " + f"{float(row['empirical_deficit']):.6f} & " + f"{float(row['empirical_stderr']):.2g} \\\\" + ) + lines += [r"\bottomrule", r"\end{tabular}"] + write_fragment(outdir / "cnn_initialization_rows.tex", lines) + + +def learning_rate_table(lr_dir: Path, outdir: Path) -> None: + rows = read_rows(lr_dir / "evaluation_rows.csv") + regime_order = {"same_lr": 0, "initial_compensation": 1, "independently_tuned": 2} + regime_label = { + "same_lr": "Same rate", + "initial_compensation": "Initial compensation", + "independently_tuned": "Independently selected", + } + cells: dict[tuple[int, str], dict[int, list[float]]] = defaultdict(lambda: defaultdict(list)) + fa_rates: dict[tuple[int, str], list[float]] = defaultdict(list) + bp_rates: dict[tuple[int, str], set[float]] = defaultdict(set) + for row in rows: + assert row["stable"] == "True", row + key = (int(row["depth"]), row["regime"]) + cells[key][int(row["init_seed"])].append(float(row["gap_to_bp"])) + fa_rates[key].append(float(row["fa_lr"])) + bp_rates[key].add(float(row["bp_lr"])) + lines = [ + r"\begin{tabular}{@{}rlcccc@{}}", + r"\toprule", + r"Depth & Regime & BP rate & Mean FA rate & Gap & SE \\", + r"\midrule", + ] + for (depth, regime), by_init in sorted( + cells.items(), key=lambda item: (item[0][0], regime_order[item[0][1]]) + ): + init_means = [statistics.mean(values) for _, values in sorted(by_init.items())] + (bp_rate,) = bp_rates[(depth, regime)] + lines.append( + f"{depth} & {regime_label[regime]} & {bp_rate:g} & " + f"{statistics.mean(fa_rates[(depth, regime)]):.4g} & " + f"{statistics.mean(init_means):.5f} & {sem(init_means):.2g} \\\\" + ) + lines += [r"\bottomrule", r"\end{tabular}"] + write_fragment(outdir / "learning_rate_regimes.tex", lines) + + +def main() -> None: + args = parse_args() + initialization_table(args.depth_dir, args.outdir) + finite_time_table(args.depth_dir, args.outdir) + cnn_table(args.cnn_dir, args.outdir) + learning_rate_table(args.lr_dir, args.outdir) + + +if __name__ == "__main__": + main() |
