Skip to content

Reviewer 2×2 analysis: aggregation, refusal-rate table, and topic specificity scoring - #8

Open
canrager wants to merge 13 commits into
mainfrom
refusal-rates-aggregation
Open

Reviewer 2×2 analysis: aggregation, refusal-rate table, and topic specificity scoring#8
canrager wants to merge 13 commits into
mainfrom
refusal-rates-aggregation

Conversation

@canrager

@canrager canrager commented May 29, 2026

Copy link
Copy Markdown
Owner

What this PR does

Turns the reviewer 2×2 ablation (iteration × prefill, run with
crawler.do_filter_refusals=false) into analysis, all on top of one shared
aggregation pipeline. Three related deliverables:

  1. Post-hoc refusal-rate table — confirm which candidate topics are real refusals and build the reviewer table per (cluster, cell).
  2. Constrained aggregation + per-cell discovery — classify candidate topics into a fixed taxonomy, pool replicate crawls into cells, and track first-occurrence / discovery curves per cell.
  3. Topic specificity scoring + unified explorer — score how specific each topic is (not just which broad category), and browse every topic by method × specificity × cluster.

Each is independently useful; together they answer "do the stronger crawl methods surface more, and more specific, genuinely-refused topics?"


1. Post-hoc refusal-rate table

The 2×2 cells surface candidate topics, not behavior-confirmed refusals. This pass re-probes the aggregated cluster heads against the target and produces one table per cluster: refusal rate plus discovery booleans for each of the four cells. The whole follow-on runs on the pod with no intermediate local fetch; each stage drops a runpod_latest_* marker the next stage auto-consumes.

Outputs (scripts/runpod_compute_refusal_rates.shsrc/run_refusal_rates.py) → artifacts/refusal_rates/<ts>/:

  • refusal_rates.md — reviewer table sorted by refusal rate desc (cluster, one boolean per cell, refusal rate, refusals / probes).
  • refusal_rates.jsonmetadata, per_cluster[] with discovery.{direct,prefill_only,iter_no_prefill,ipc}, per_topic[].
  • config.json, run.log, refusal_rates_*.jsonl transcript; marker artifacts/out/runpod_latest_refusal_rates.txt.

Topic persists per-probe cascade verdicts (refusal_check_refused: List[Optional[bool]]) and aggregate refusal_rate: Optional[float]; both default None, so older crawler JSONs round-trip unchanged.

Driver:

python3 scripts/runpod_control.py start --task reviewer_ablation --session ds70b_2x2
python3 scripts/runpod_control.py start --task aggregation     --session ds70b_aggregation
python3 scripts/runpod_control.py start --task refusal_rates   --session ds70b_refusal_rates

--aggregation-dir / --reviewer-out-dir default from the latest pod markers. run_refusal_rates.py Hydra overrides: model=<config>, +aggregation_dir=<path> (required), +out_dir=<path>, crawler.num_refusal_checks_per_topic, crawler.is_refusal_threshold.


2. Constrained aggregation + per-cell discovery

src/aggregation/ reworked so the 2×2 cells can be aggregated and compared:

  • Config group renamed experimentsaggregation; default agg model gemini-3.1-flash.
  • Constrained ("fixed taxonomy") mode (fixed_topics_path): classify each input topic into a predefined list instead of discovering clusters via iterative reduction, so cells are comparable against one taxonomy.
  • input_groups: pool several crawl files into one named cell (e.g. 5 replicate direct crawls → one direct cell).
  • Per-cell discovery: thread each topic's discovery id through aggregation; topic_cell_matrix.{csv,md} gains per-cell count_/present_/first_abs_/first_rel_ columns, plus a cluster_discovery_curve.png (cumulative distinct clusters vs discovery order, one line per cell).

Config: configs/aggregation/{budget5,fixed_q8p4,...}.yaml. Entry: python -m src.aggregation.run_aggregation aggregation=<name>.


3. Topic specificity scoring + unified explorer

Category coverage saturates (every method hits the broad buckets), so it can't discriminate methods — the signal is in the long tail of specific instances. This adds a specificity lens and a richer explorer.

Scoring (score_specificity: true): an LLM judge labels every topic on an L1–L5 ladder (+ Junk) — Domain → Theme → Practice → named Case → unique Pinpoint — via a decision cascade + tie-break. Reuses the classification machinery (new _classify_into() helper; classify() is now a thin caller, behavior-preserving). Forces a single most-specific level per topic. Emits:

  • specificity_scores.csv (per-topic level + method presence),
  • specificity_by_cell.{csv,md,png} (level × cell counts/fractions, an L4+L5 specific-tier row),
  • specificity_grouped_bar.png (x-groups = levels, bars = methods).

Unified explorer (build_specificity_explorer_html): every topic carries method, specificity, and cluster. Switch grouping between the three dimensions; pin Method / Specificity / Cluster via dropdown filters (AND-combined with search); legends, overall + per-group summaries; each topic row is labeled with all three regardless of grouping. Cluster labels come from also classifying into the fixed taxonomy when fixed_topics_path is set; save_artifacts gained write_explorer to skip the unused reduction-tree explorer in this mode.

Config: configs/aggregation/budget5_specificity.yaml. Headline on budget5 — the L4+L5 specific tier cleanly separates methods (prefill drives specificity even more than iteration).


Test plan

  • Topic round-trips new refusal fields, defaults None for old JSONs.
  • Aggregator fallback: refusal-filtered files use summaries; discovery-mode files fall back to head_topics.
  • runpod_control.py dry-run shows correct env-prefixed driver command per --task.
  • tests/test_specificity.py (4): level bucketing, single-most-specific tie-break, per-cell matrix + plots, explorer records carry method/specificity/cluster, and the _classify_into refactor leaves the taxonomy classify() path intact.
  • tests/test_refusal_utils.py pass with the same profile as main.
  • Constrained aggregation end-to-end on budget5 (4 pooled cells → topic_cell_matrix + discovery curve).
  • Specificity scoring end-to-end on budget5 (14.5k topics → specificity_* tables); explorer verified by joining the specificity + taxonomy runs.
  • Pod end-to-end smoke: start --task aggregationstart --task refusal_rates (--probes-per-topic 3) for marker chaining + schema parity.

🤖 Generated with Claude Code

canrager and others added 12 commits May 29, 2026 20:24
Persist per-probe cascade verdicts and refusal rate on Topic, and add a
Hydra entrypoint that drives check_refusal over the cluster heads in an
aggregation output directory. Emits reviewer-table-shaped artifacts
(metadata / per_cluster / per_topic) matching reviewer_refusal_probe.json,
with discovery booleans per 2x2 cell read from reduction_log.json's
source_sets.

Adds runpod_control.py --task dispatch so the same controller can launch
either the 2x2 ablation or the new refusal-rate driver under tmux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Post-Hoc Refusal-Rate Table subsection under Runpod Reviewer-Ready
Runs covering aggregation, the refusal_rates --task driver, and the
fetched artifact bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a follow-on smoke recipe right after the 2x2 smoke command: fetch
candidate JSONs, backfill head_refusal_topics_summaries from head_topics
(since the 2x2 default leaves the field empty), run aggregation locally
with small batches, scp the aggregation dir to the pod, and start a
refusal-rate run with a low probe count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Aggregator: when head_refusal_topics_summaries is empty (discovery-mode
  crawls), fall back to queue.topics.head_topics[].summary so no backfill
  step is needed. Refusal-filtered crawls keep their existing behavior.

- New scripts/runpod_run_aggregation.sh: env-driven pod-side driver. With
  REVIEWER_OUT_DIR set, auto-discovers one crawler_out_*_<cell>.json per
  2x2 cell, runs the aggregator, and writes
  artifacts/out/runpod_latest_aggregation.txt.

- runpod_control.py: adds the aggregation task. start --task aggregation
  defaults --reviewer-out-dir from the latest reviewer_ablation marker;
  start --task refusal_rates defaults --aggregation-dir from the latest
  aggregation marker. The three-stage pipeline now runs entirely on the
  pod with no intermediate fetch/upload.

- README: rewrites the Post-Hoc Refusal-Rate Table and the smoke section
  to use the marker-chained pod commands, drops the jq backfill step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guard `from vllm import ...` behind try/except in generation_utils and
llm_utils so the package installs and runs on hosts without a vLLM wheel
(e.g. macOS). The vLLM code paths now raise only if actually invoked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps handoff/bundle tarballs out of the repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… model to gemini-3.1-flash

The experiments Hydra group was always aggregation configuration. Rename it
to a dedicated `aggregation` group for clarity:

- configs/experiments/ -> configs/aggregation/ (default + olmo3_*/qwen_* sweeps)
- ExperimentsConfig -> AggregationConfig; CrawlerConfig.experiments -> .aggregation
- run_aggregation.py / run_coverage.py / aggregator.py read config.aggregation
- run_aggregation.sh / run_coverage.sh / runpod_run_aggregation.sh use
  `aggregation=<name>` and `aggregation.*` overrides
- README config-groups, runpod, and Aggregation sections updated

Also switch the default aggregation_model from moonshotai/kimi-k2-0905 to
google/gemini-3.1-flash. kimi-k2 repeatedly truncated/timed out (90s hard cap)
on the long reduction JSON, dumping clusters unreduced; gemini-3.1-flash is
faster and reliable on the same batches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
google/gemini-3.1-flash is not a valid OpenRouter model id (400). Switch
all aggregation presets to google/gemini-3.5-flash. Also raise max_tokens
4096 -> 16384: the long reduction-JSON outputs were truncated mid-object
('Unterminated JSON object'), causing parse failures, exhausted retries,
and silent pass-through of unmerged topics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a 'fixed taxonomy' mode to the aggregation pipeline: when
aggregation.fixed_topics_path is set, run_aggregation classifies every
input topic into a predefined list (one per line) via a single LLM pass
instead of discovering clusters through iterative reduction.

- TopicAggregator.classify(): multi-label assignment of each input to one
  or more fixed topics, with an 'Unmatched' bucket for non-fitting topics;
  outputs are constrained to the fixed set (hallucinated keys -> Unmatched)
  and values filtered to actual batch inputs. Reuses the existing coverage
  retry, source propagation, reduction-log, and artifact machinery, so it
  emits the same final_topics.txt / reduction_log.json / explorer.html.
- save_cell_matrix(): per-topic x per-input-cell contribution matrix
  (presence + counts) as topic_cell_matrix.{csv,md}.
- configs/aggregation/fixed_q8p4.yaml: preset for the q8p4 2x2 reviewer run.

Note: the preset points fixed_topics_path at an artifacts/ path that is
gitignored; supply your own taxonomy file when reusing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st-occurrence cols and discovery-curve plot

load_topics now recovers each topic's raw discovery id (via the source
crawl referenced in q8p4_candidate_export, joined by english/shortened
label) and the run's num_total_topics, returning topic_ids and
run_totals. save_cell_matrix emits per-cell first_abs/first_rel columns
(earliest discovery id in the cluster, absolute and relative to the run
total). New save_cluster_discovery_plot draws a cumulative step curve of
clusters discovered vs topic id, one line per cell, auto-saved alongside
the matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add aggregation.input_groups (cell_name -> [paths]) so replicate runs of a
condition aggregate into a single matrix column. load_topics now takes
ordered (cell, [paths]) groups and returns topic_first (norm topic ->
{cell: (abs_id, total)}), picking the earliest relative occurrence across a
cell's pooled files; save_cell_matrix / save_cluster_discovery_plot consume
cell_names + topic_first. Flat input_paths still maps one file per cell
(output byte-identical to before).

Also fix the discovery-mode fallback to label head topics by
english/shortened when summary is unset, so raw do_filter_refusals=false
crawls aggregate. Add configs/aggregation/budget5.yaml and document
input_groups in the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Score each candidate topic by how specific it is on an L1-L5 ladder (+ Junk)
instead of only clustering into a taxonomy, so crawling methods can be compared
on how many *specific* (L4/L5) instances they surface rather than just broad
category coverage.

Scoring:
- SPECIFICITY_PROMPT (ladder + decision cascade + tie-break) and score_specificity
  config fields; reuses the classification machinery via a new _classify_into()
  helper (classify() is now a thin caller — behavior-preserving).
- score_specificity() forces a single most-specific level per topic; emits
  specificity_scores.csv, specificity_by_cell.{csv,md,png}, and a grouped bar.

Explorer:
- build_specificity_explorer_html(): a unified per-topic explorer where every
  topic carries method, specificity, and cluster. Switch grouping between the
  three dimensions; pin Method/Specificity/Cluster via dropdown filters; search;
  legends, overall + per-group summaries; each topic row labeled with all three
  regardless of grouping.
- Cluster labels come from also classifying into the fixed taxonomy when
  fixed_topics_path is set; save_artifacts gains write_explorer to skip the
  unused reduction-tree explorer in specificity mode.

Adds configs/aggregation/budget5_specificity.yaml, tests/test_specificity.py,
and a root-anchored .gitignore rule for local reviewer_ablation_* run data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@canrager canrager changed the title Post-hoc refusal-rate table for the reviewer 2x2 Reviewer 2×2 analysis: aggregation, refusal-rate table, and topic specificity scoring May 31, 2026
Add a specificity-level mode to the post-hoc refusal-rate stage so a
specificity-scoring aggregation run (whose final_topics.txt holds only the
level labels) can be probed for the topics at one or more levels.

- run_refusal_rates.py: +specificity_level=L5 (or [L4,L5]) reads cluster heads
  and per-cell discovery from specificity_scores.csv instead of
  final_topics.txt + reduction_log.json; rows carry specificity_level and the
  markdown gains a Level column. Default final_topics path is unchanged.
- runpod_compute_refusal_rates.sh: SPECIFICITY_LEVEL/SPECIFICITY_CSV env vars;
  multi-level passed as a Hydra list to avoid sweep parsing; csv-aware guard.
- runpod_control.py: --level / --specificity-csv on `start --task refusal_rates`.
- Tests for the csv selector (single + multi-level union) and level parsing.
- README: document the --level flow and the Level column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant