summaryrefslogtreecommitdiff
path: root/scripts/generate_paper_tables.py
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-07-28 21:17:38 +0800
committerhaoyuren <13851610112@163.com>2026-07-28 21:17:38 +0800
commit3b5f01cb4db9efc4ceb2ba17709bbfc82a63e660 (patch)
treead4e2321373fc9e551ffeb2ca27f5533ca9e9bdb /scripts/generate_paper_tables.py
parent18bf25f34f6d352605086aa8fe63977baacc315b (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.py177
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()