MultiBBQ's evaluation surface is data-driven: experiments, metrics, and bias categories are all table/data entries, not code paths. This document is the reference for the three extension points beyond adding a model (for that, see evaluate-your-own-model.md), each in Where / The contract / Validate form.
| # | Extension point | Primary file(s) | Selected by |
|---|---|---|---|
| 1 | New experiment | ../../multibbq/experiments.py | --experiment |
| 2 | New metric field | ../../multibbq/metrics/scorer.py + ../../multibbq/cli.py | --score |
| 3 | New bias category | the data (category field) + --categories |
data + CLI flag |
The single EXPERIMENTS dict in
../../multibbq/experiments.py. Each key is one
--experiment value; the value is a dict of orthogonal axes plus a results
token. The unified inference loop in
../../multibbq/inference.py reads these keys, so there is no
per-experiment script to add.
Add one row. Every axis key:
| Key | Values | Meaning |
|---|---|---|
mode |
default | temp |
Model wrapper mode (reasoning is resolved at runtime from --reasoning_mode, so use default + retry=True for reasoning experiments). |
quant |
bool |
Load the model quantized. |
fields |
masked | unmasked |
masked uses the demographic-neutral *_masked text columns (MultiBBQ's language-leakage control); unmasked uses the raw BBQ columns. |
image |
dataset | aug | blank | realworld | none |
Image source: the generated image, a perturbed variant, a white canvas, a real-world photo, or no image (text-only). |
options |
plain | label |
plain shows the (masked) choices; label replaces the non-answer options with person A/B/C. |
disambig |
plain | masked |
Which disambiguating-context column to use (img_label uses disambig_context_masked). |
inject |
bool |
Re-inject demographic names into the ambiguous context (the context_unmasked leakage control). |
vo |
bool |
Whether the visual-only split is supported for this setting. |
retry |
bool |
Wrap model.run in the 3-attempt retry loop (needed for long reasoning outputs that intermittently return None). |
token |
str | Results subdirectory: results/<data_id>_<token>/. Overridden at runtime for some experiments: image="aug" uses --img_aug_type, temp uses temp_<temperature>, and reasoning uses --reasoning_mode. |
text_only |
bool (optional) |
Route through the factory's text_only=True path (HFTextModel, no vision wrapper). Used by llm. |
strip_image_ref |
bool (optional) |
Strip " in the image" from the question (for text-only eval where there is no image). Used by llm. |
Example (a new "grayscale image" study reusing the baseline text setup):
EXPERIMENTS = {
...
"gray_img": dict(mode="default", quant=False, fields="masked", image="aug",
options="plain", disambig="plain", inject=False, vo=True,
retry=False, token="aug"),
}Because image="aug", the harness resolves both the image tree and the results
subdirectory from --img_aug_type (the registry token is overridden at runtime):
place your grayscale copies under data/images/gpt_image_gen_gray/, mirroring the
main set's layout, and add "gray" to the --img_aug_type choices in the run
parser. The --experiment flag itself needs no CLI edit:
run's parser declares --experiment choices=sorted(EXPERIMENTS) in
../../multibbq/cli.py, so a new key becomes a valid value
automatically. If your experiment needs a brand-new per-run knob, add the argparse
argument to the run parser and read it in inference.py.
multibbq run org/MyModel-7B --experiment gray_img --data_id gpt_image_gen --img_aug_type grayConfirm results land under results/gpt_image_gen_gray/… and that scoring succeeds
(running.md, metrics.md). Cross-check against
experiments.md, which documents each shipped row.
Scoring lives in ../../multibbq/metrics/scorer.py.
Both entry points (eval_visual_only(data, neg, tail_slice=None) and
eval_visual_language(data, ambig, neg, tail_slice=None)) return the same schema:
{
"overall": {"fairness_score", "bias_score", "unk_rate"},
"by_category": {<category>: {"fairness_score", "bias_score", "unk_rate"}, ...},
}To add a field (say err_rate, the -1/unparseable rate):
- Compute it in both scorers. Accumulate the per-row count in the
categoryloop, then emit it in the per-categorycategory_metrics[category]dict and in theoveralldict, mirroring howfairness_score/bias_score/unk_rateare computed with_safe_div. Keep both scorers in sync; they share the schema. - Surface it via the CLI.
--scorein thescoresubparser (../../multibbq/cli.py) ischoices=["all", "fairness", "bias", "unk"], resolved by_resolve_score_flagagainst the mapping{"fairness": "fairness_score", "bias": "bias_score", "unk": "unk_rate"}. Add your key to_SCORE_KEYS, add achoicesentry, and add its mapping row._filter_scoresthen prunes to the selected key(s) for bothoverallandby_categoryautomatically. - (If it should reach the CSVs) the aggregate layer
(../../multibbq/metrics/aggregate.py) reads
only
fairness_score/bias_scoreinto its BBQ-shaped MultiIndex CSVs. A new field surfaces throughscore/combinebut will not appear in the aggregated CSVs unless you extend those builders too.
Score a reference file and diff the new field against a hand-computed value:
multibbq score -i results/gpt_image_gen_main/org/MyModel-7B/<one>.json -o /tmp/ref_w_metrics.json --score allCheck the printed overall / by_category block includes your field with the
expected value.
Categories are pure data; nothing in the scorer enumerates them. Each result row
carries a category string; the scorers group on row["category"]
(category_data[row["category"]].append(row)), so a new category value appears in
by_category automatically the moment rows carry it.
-
Add data rows with your new
categoryvalue (e.g."disability"), alongside the existing fairness fields each row needs (pred,stereotype_group_idx,nonstereotype_group_idx,unk_label_idx, andcorrect_option_idxfor visual-language). The scorer's per-category grouping requires no code change. -
Pass it to aggregate. The aggregate/pipeline steps take an explicit
--categorieslist (default["gender", "race", "religion", "age"]in_add_metric_common, ../../multibbq/cli.py). Category CSVs and thecategory_total_scores_summary.csvonly include categories you name:multibbq pipeline -i results/... -o out/ \ --categories gender race religion age disabilitycreate_all_category_metrics_csvswrites one CSV per category found in the data, butcal_total_scores/combine_category_totalsonly process the--categoriesyou pass (andoverall). If you also want it in the per-main-category average CSV, add its title-cased form to--subcategory-order.
multibbq score -i results/... -o out/analysis # per-category scores appear
multibbq aggregate -i out/combined_metrics.json \
--csv-dir out/csv --metrics-dir out/metrics \
--categories gender race religion age disabilityConfirm a disability_metrics_summary_sorted.csv is written and a disability
column appears in category_total_scores_summary.csv. See metrics.md
for the full scoring → combine → aggregate pipeline.