doc-to-lora/scripts/main_exp/eval/d2l-plot-chunking-abalation.py
2026-06-15 04:31:47 +00:00

248 lines
7.8 KiB
Python

from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
import matplotlib
import matplotlib as mpl
matplotlib.use("Agg")
import matplotlib.pyplot as plt
sys.path.append(str(Path(__file__).resolve().parents[2] / "niah"))
from plot_ablation_utils import ( # noqa: E402
choose_latest_runs_by_chunk,
ensure_output_path,
get_performance_value,
load_results_json,
load_script_text,
parse_checkpoint_path,
parse_eval_results_root,
)
LATTE_STYLE = "https://raw.githubusercontent.com/51616/catppuccin-matplotlib/main/src/mplcatppuccin/data/latte.mplstyle"
DATASET_LABELS = {
"longbench/qasper_e": "Qasper",
"longbench/2wikimqa_e": "2WikiMQA",
"longbench/multifieldqa_en_e": "MultiFieldQA-EN",
"longbench/gov_report_e": "GovReport",
}
REFERENCE_PERFORMANCE = {
"longbench/2wikimqa_e": 0.3387,
"longbench/multifieldqa_en_e": 0.3938,
"longbench/qasper_e": 0.3839,
}
METRIC_CANDIDATES = (
"qa_f1_score",
"rougeL.f1",
"qa_f1",
"f1",
"accuracy",
)
def chunk_size_tick_label(chunk_size: int) -> str:
return rf"$2^{{{int(chunk_size).bit_length() - 1}}}$"
def approx_chunk_label(value: float) -> str:
rounded = round(value)
if abs(value - rounded) < 0.05:
return str(int(rounded))
return f"{value:.1f}"
def configure_plot_style() -> None:
plt.style.use(["ggplot", LATTE_STYLE])
plt.rcParams["axes.facecolor"] = "F7FBFC"
plt.rcParams["figure.facecolor"] = "white"
plt.rcParams["savefig.facecolor"] = "white"
plt.rcParams["grid.color"] = "cccccc"
plt.rcParams["grid.linewidth"] = 1
plt.rcParams["axes.edgecolor"] = "ccd0da"
plt.rcParams["legend.facecolor"] = "white"
plt.rcParams["legend.fontsize"] = 14
plt.rcParams["xtick.labelsize"] = 13
plt.rcParams["ytick.labelsize"] = 13
plt.rcParams["axes.labelweight"] = "bold"
plt.rcParams["axes.prop_cycle"] = mpl.cycler(color=plt.cm.tab10.colors)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Plot one D2L chunking-ablation figure per dataset.",
)
parser.add_argument(
"--eval-script",
type=Path,
default=Path(__file__).with_name("d2l-chunking-abalation.sh"),
help="Path to the eval sweep shell script.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=None,
help="Optional directory for output PNGs. Defaults to eval-results-*/plots/.",
)
return parser
def parse_checkpoint_from_script(script_text: str) -> Path:
try:
return parse_checkpoint_path(script_text)
except ValueError:
match = re.search(r"--checkpoint_path\s+(\S+)", script_text)
if match is None:
raise
return Path(match.group(1))
def infer_metric_name(run_dir: Path, dataset_name: str) -> str:
result_json = load_results_json(run_dir, dataset_name)
for metric_name in METRIC_CANDIDATES:
if f"test_{dataset_name}_{metric_name}" in result_json:
return metric_name
raise ValueError(f"Could not infer a metric for dataset {dataset_name}.")
def dataset_label(dataset_name: str) -> str:
return DATASET_LABELS.get(dataset_name, dataset_name.split("/")[-1])
def parse_datasets_from_script(script_text: str) -> list[str]:
match = re.search(r"--datasets\s+(.+?)\s+--split", script_text, flags=re.DOTALL)
if match is None:
raise ValueError("Could not parse datasets from the eval script.")
return match.group(1).split()
def mean_context_length(run_dir: Path, dataset_name: str) -> float:
path = run_dir / f"test_{dataset_name}_generated_text.jsonl"
total = 0.0
count = 0
with path.open(encoding="utf-8") as handle:
for line in handle:
record = json.loads(line)
ctx_ids_len = record.get("ctx_ids_len")
if isinstance(ctx_ids_len, (int, float)):
total += float(ctx_ids_len)
count += 1
if count == 0:
raise ValueError(f"Could not find ctx_ids_len values in {path}.")
return total / count
def main() -> None:
args = build_parser().parse_args()
configure_plot_style()
script_text = load_script_text(args.eval_script)
checkpoint_path = parse_checkpoint_from_script(script_text)
eval_results_root = parse_eval_results_root(checkpoint_path)
expected_datasets = parse_datasets_from_script(script_text)
selected_runs = choose_latest_runs_by_chunk(eval_results_root, expected_datasets)
if not selected_runs:
raise SystemExit(f"No matching eval runs found under {eval_results_root}.")
output_dir = (
eval_results_root / "plots" if args.output_dir is None else args.output_dir
)
output_dir.mkdir(parents=True, exist_ok=True)
for dataset_name in expected_datasets:
metric_name = infer_metric_name(selected_runs[0].run_dir, dataset_name)
reference_performance = REFERENCE_PERFORMANCE.get(dataset_name)
chunk_sizes: list[int] = []
approx_num_chunks: list[float] = []
normalized_performance_values: list[float] = []
for run_info in selected_runs:
chunk_size = run_info.chunk_size
if chunk_size == 65536:
continue
performance = get_performance_value(
run_info.run_dir,
dataset_name,
metric_name=metric_name,
)
if performance is None:
continue
avg_context_length = mean_context_length(run_info.run_dir, dataset_name)
chunk_sizes.append(chunk_size)
approx_num_chunks.append(avg_context_length / chunk_size)
plot_value = (
performance / reference_performance
if reference_performance not in (None, 0)
else performance
)
normalized_performance_values.append(plot_value)
if not chunk_sizes:
continue
fig, ax_perf = plt.subplots(
1,
1,
figsize=(7.5, 5.5),
constrained_layout=False,
)
fig.subplots_adjust(bottom=0.18, top=0.86)
ax_perf.plot(
chunk_sizes,
normalized_performance_values,
marker="o",
linewidth=2,
color="#1f77b4",
markeredgecolor="black",
markeredgewidth=1,
)
for chunk_size, performance, approx_chunks in zip(
chunk_sizes,
normalized_performance_values,
approx_num_chunks,
):
ax_perf.annotate(
approx_chunk_label(approx_chunks),
xy=(chunk_size, performance),
xytext=(0, 7),
textcoords="offset points",
ha="center",
va="bottom",
fontsize=11,
color="#4c4f69",
)
ax_perf.grid(True, alpha=0.25)
ax_perf.set_xscale("log", base=2)
ax_perf.set_xticks(chunk_sizes)
ax_perf.set_xticklabels(
[chunk_size_tick_label(chunk_size) for chunk_size in chunk_sizes]
)
ax_perf.xaxis.set_minor_locator(plt.NullLocator())
pretty_name = dataset_label(dataset_name)
ax_perf.set_title(f"Retrieval Performance\n({pretty_name})", fontweight="bold")
ax_perf.set_xlabel("Chunk Size (tokens)")
ax_perf.set_ylabel(
"Normalized Performance"
if reference_performance not in (None, 0)
else "Performance"
)
ax_perf.set_ylim(bottom=0.0)
output_path = ensure_output_path(
output_dir / f"d2l-chunking-abalation-{dataset_name.split('/')[-1]}.png"
)
fig.savefig(output_path, dpi=200, bbox_inches="tight")
print(f"Saved plot to {output_path}")
if __name__ == "__main__":
main()