Skip to content

Adds grid stratification, acceptability model, and full layers interop (0.8.0)#9

Merged
aaronstevenwhite merged 9 commits into
mainfrom
feat/nd-grid-stratification
Jul 13, 2026
Merged

Adds grid stratification, acceptability model, and full layers interop (0.8.0)#9
aaronstevenwhite merged 9 commits into
mainfrom
feat/nd-grid-stratification

Conversation

@aaronstevenwhite

@aaronstevenwhite aaronstevenwhite commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Description

Releases bead 0.8.0. Three interlocking pieces plus the training fixes surfaced while verifying them:

  • N-dimensional, mixed-variable grid stratification (bead.lists): a BinningSpec tagged union (quantile, equal-width, threshold, standard-deviation, categorical), assign_grid_cells / assign_grid_cells_by_uuid / grid_shape / flatten_cell / unflatten_cell, a GridStratificationConstraint, and a stratum-aware QuantileBalancer.balance_by_cell with a matching ListPartitioner path. The one-dimensional assign_quantiles is now the quantile specialization of this single engine.
  • Acceptability model + scorer: AcceptabilityScorer (bead.items.scoring) scores a forced-choice item by a trained ForcedChoiceModel. The eng/argument_structure gallery trains that model on MegaAcceptability (per-annotator Likert to 2AFC, with a by-sentence held-out dev set and early stopping), stratifies 2AFC pairs across an acceptability x language-model grid, and seeds the active-learning loop with the pretrained model.
  • Full layers/lairs interop: new lenses for filled templates (FILLED_TEMPLATE_FILLING), judgments (ANNOTATION_RECORD_JUDGMENT + JudgmentSet), list constraints (LIST_CONSTRAINT), and experiment lists (EXPERIMENT_LIST_LAYERS), all exact round-trips. Every gallery artifact (lexicons, templates, filled templates, cross-product items, 2AFC pairs, training corpus, experiment lists) is persisted through the layers schema.
  • Forced-choice training fixes (found while verifying real training): train/validation metrics are computed from the model's own predict path (the shared mixed-effects trainer cannot score forced choice, so train accuracy previously always read 0.0); evaluate's F1 no longer receives an unsupported zero_division; and per-participant random-effects tensors are placed on the model device, so training and prediction run on MPS and CUDA, not only CPU.

A bead Participant is intentionally not mapped to a layers persona.Persona (Persona is an annotator role, not a concrete participant); judgment.AgreementReport has no bead model to lens.

Motivation

Bring gallery/eng current with the 0.7.0 layers interop, add an acceptability-judgment model (trained on MegaAcceptability) to drive item stratification, and generalize stratification from a single quantile dimension to an arbitrary grid of continuous and discrete variables.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Tests (adding or updating tests)

Checklist

  • I have read the CONTRIBUTING guidelines
  • My code follows the project's style guidelines
  • I have run uv run ruff check . and uv run ruff format .
  • I have run uv run pyright with no errors
  • I have added tests that prove my fix/feature works
  • All tests pass (uv run pytest tests/)
  • I have updated documentation as needed

Testing

  • pyright bead/: 0 errors; ruff check + ruff format --check clean across bead/.
  • Full suite green: 3501 passed (standard) + 257 passed (slow model-training marker) = 3758, 0 failures.
  • New tests: grid stratification (tests/lists/test_grid_stratification.py), AcceptabilityScorer (tests/items/test_acceptability_scorer.py), and the layers lenses (tests/interop/test_layers_resource.py, test_layers_judgment.py, test_layers_list.py), all exact round-trips.
  • Real training verified end to end with bert-base-uncased on CPU and bert-tiny on MPS: the fit learns (loss drops, train/dev accuracy above chance), dev-set early stopping engages and restores the best weights, and the scorer and active-learning loop both run on the trained model.

Screenshots (if applicable)

N/A

….8.0)

Adds N-dimensional, mixed-variable grid stratification to bead.lists: a
BinningSpec tagged union (quantile, equal-width, threshold, standard-deviation,
and categorical binners), assign_grid_cells / assign_grid_cells_by_uuid /
grid_shape / flatten_cell / unflatten_cell, a GridStratificationConstraint, and
a stratum-aware QuantileBalancer.balance_by_cell with a matching ListPartitioner
path. The one-dimensional assign_quantiles is now the quantile specialization of
the single engine.

Adds AcceptabilityScorer (bead.items.scoring), which scores a forced-choice item
by a trained ForcedChoiceModel and returns the predicted preference margin.

Extends bead.interop.layers with lenses for every mappable bead model: a
FilledTemplate to a resource.Filling, an AnnotationRecord to a judgment.Judgment
(grouped per annotator into a judgment.JudgmentSet), a ListConstraint to a
judgment.ListConstraint, and an ExperimentList to a resource.Collection of kind
stimulus-pool with one membership per item and its list constraints. All
round-trip exactly. A Participant is intentionally not mapped: a persona.Persona
is an annotator role, not a concrete participant, whose identity is an AgentRef.

Rebuilds the gallery/eng/argument_structure example: an acceptability model
trained on MegaAcceptability (per-annotator Likert to 2AFC pairing with rater
random effects) scores items, the 2AFC pairs are stratified across a grid of the
acceptability margin, the language-model score, and the pair type, and the
active-learning loop is seeded with the pretrained model. Every artifact
(lexicons, templates, filled templates, cross-product items, 2AFC pairs, the
training corpus, and experiment lists) is persisted through the layers schema
and the bead lairs codec.

Bumps the version to 0.8.0. See CHANGELOG for the full entry.
Running the loop surfaced two blockers in run_pipeline.py. First, the model
validates each item against the template's task_spec.options and reads those
option names from rendered_elements, but the protocol template leaves options
unset (FORCED_CHOICE uses per-item options). build_item_template now sets
("option_a", "option_b") on the model-facing template so the 2AFC pairs
validate. Second, the loop trains without participant ids, which a
random-intercepts model rejects; load_acceptability_model now switches the
seeded model to fixed effects (the pretrained encoder carries over) so the loop
fine-tunes at the population level.

Verified end to end with bert-base-uncased on CPU: initial training
(ForcedChoiceModel.train with random intercepts), AcceptabilityScorer scoring
via the population fallback, and a full loop.run iteration with the loaded model.
…bility model

Running real training surfaced several issues, fixed here.

ForcedChoiceModel reported train_accuracy as 0.0 because it read accuracy from the
shared mixed-effects trainer's evaluate(), whose prediction step only labels cloze
batches, so for forced choice it returns no usable accuracy or loss. Train and dev
metrics are now computed from the model's own predict path (which applies the
fitted random effects), so they reflect the deployed model. compute_binary_metrics
and compute_multiclass_metrics no longer pass zero_division to evaluate's f1, which
this version rejects.

The model now supports early stopping on a held-out dev set via a callback that
scores the dev set through the predict path at each epoch end, stops after
early_stopping_patience epochs without dev cross-entropy improvement, and restores
the best encoder and head. A new early_stopping_patience config field controls it.

The eng gallery builds the dev set by holding out whole sentences:
prepare_megaacceptability.py splits unique sentences (and all their ratings) into
train and dev so the dev set contains only sentences never seen during training,
and writes both; train_acceptability_model.py loads the dev set and passes it for
early stopping. The gallery JSONL loaders now use Item.model_validate_json, since
Item(**json.loads(line)) no longer coerces string UUIDs under didactic.

Verified end to end on bert-base-uncased: training learns (loss drops, train and
dev accuracy above chance), early stopping engages, and the existing forced-choice,
categorical, and binary model tests pass.
…ame pairs

MegaAcceptability lists are structured so no verb or frame repeats within a list,
so the only within-rater controlled contrast is same-frame (different verbs, one
frame), which a rater sees once per list across their lists. Capping at 200 pairs
per annotator discarded most of these, so max_pairs_per_annotator now defaults to
null (no cap). On the full dataset this raises the derived same-frame training set
from roughly 145k pairs to roughly 760k (minus ties and the held-out dev split).
…and CUDA

Switching the acceptability model to device: mps surfaced a device mismatch:
_do_predict_proba adds the per-participant random-effects intercept, a CPU tensor,
to the encoder logits on the accelerator, raising "Expected all tensors to be on
the same device". The random-effects bias (and the random-slopes head) are now
moved to the logits / embedding device, which is a no-op on CPU. The early-stopping
callback methods now return None and mutate control in place, matching the
TrainerCallback signature.

The eng gallery config sets acceptability_model.device to mps, since the uncapped
same-frame training set (~760k pairs) is impractical on CPU.

Verified: BERT trains and scores on MPS, and the forced-choice model tests still
pass on CPU.
layers represents a data producer as an AgentRef (identity), separate from the
persona (role/framework) and tool (software), and documents JudgmentSet.features
as the home for annotator demographics, session metadata, and payment info. Map
a bead Participant onto that: PARTICIPANT_AGENT lenses Participant <-> AgentRef
losslessly, participant_features renders the study fields as a FeatureMap, and
records_to_judgment_set takes an optional participant so a judgment set carries
the annotator's identity and study fields natively.
@aaronstevenwhite
aaronstevenwhite merged commit 7c29a97 into main Jul 13, 2026
8 checks passed
@aaronstevenwhite
aaronstevenwhite deleted the feat/nd-grid-stratification branch July 14, 2026 00:55
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