fix(metrics): make inst_level_* a micro-average over instructions - #1672
Merged
Conversation
…delscope#1671) `inst_level_strict` / `inst_level_loose` were reduced twice: `agg_inst_level_acc` collapsed each sample's instruction list into a ratio, then `Mean` averaged those ratios without weights. That is a macro-average over prompts, so a 1-instruction prompt counted as much as a 3-instruction one. The official implementation (`instruction_following_eval/evaluation_lib.py:192-193,211`) pools every instruction in the dataset and divides once. The bias is not a constant offset: it inflates models that do better on lightly-constrained prompts and deflates the opposite, so it can reorder models. Adds a `weighted_mean` aggregator that honours per-metric weights declared in `Score.metadata`, and wires up the three affected benchmarks: - ifeval, ifbench: weight by `len(instruction_id_list)` - multi_if: weight each `turn_N_inst_level_*` by that turn's instruction count, where `parse_result` on a single turn had collapsed to a per-sample ratio `prompt_level_*` keeps its unweighted mean; a metric is weighted only where a weight is declared, so both live under one aggregator. For a weighted metric `AggScore.num` reports the unit total rather than the sample count, which keeps the report layer's `micro_mean` rollup a true micro-average across subsets (multi_if has 7). A prompt with an empty instruction list now drops out instead of contributing a spurious 0. `weighted_mean` was already declared in `KNOWN_AGGREGATIONS`, and poly_math already emits this identity, so the identity axis is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related to #1671
Summary
This PR makes instruction-level accuracy pool every instruction in the dataset, matching the official IFEval definition, instead of averaging per-prompt ratios.
inst_level_strict/inst_level_looseare now a micro-average: a prompt carrying 3 instructions weighs 3x one carrying a single instruction.weighted_meanaggregator that honours per-metric weights declared inScore.metadata.ifeval,ifbench,multi_if.prompt_level_strict/prompt_level_loosekeep their unweighted mean; weighted and unweighted metrics coexist under one aggregator.0.Root Cause
The official implementation (
google-research/instruction_following_eval/evaluation_lib.py,print_reportat:170) accumulates across the whole dataset and divides once — accumulation at:192-193, output at:211:evalscope reduced twice instead:
evalscope/benchmarks/ifeval/utils.py:131-133—agg_inst_level_acccollapsed each sample's instruction list into a ratio viasum(items) / len(items).evalscope/metrics/aggregators/aggregators.py—Meanthen averaged those ratios with no weights.The composition is a macro-average over prompts, so a 1-instruction sample and a 3-instruction sample carried equal weight in the final score.
The bias is not a constant offset. It inflates models that do better on lightly-constrained prompts and deflates the opposite, so it can reorder models rather than shifting all of them together. IFEval's per-prompt instruction counts are unevenly distributed between 1 and 3, so the difference does not cancel out.
Worth noting: the correct micro logic already existed in the repository.
ifbench/evaluation_lib.pycarries the upstreamprint_reportverbatim, but it has no callers anywhere in the package — the code path that actually ran was theagg_inst_level_accone.Changes
New aggregator (
evalscope/metrics/aggregators/aggregators.py)WeightedMean, registered asweighted_mean, reads{metric_name: weight}fromScore.metadata[METRIC_WEIGHTS_KEY].1.0— decides, so a dataset whose prompts all carry exactly one instruction is still handled as a weighted metric.1.0rather than dropping out of that metric's denominator.AggScore.numreports the unit total instead of the sample count. This keeps the report layer'smicro_meanrollup a true micro-average across subsets, which matters formulti_if(7 language subsets).metadatacarriesweighted,samples, andtotal_weightso the distinction stays inspectable in a report.Benchmark wiring
ifeval,ifbench:aggregation='weighted_mean', weight= len(instruction_id_list), taken from the doc.multi_if: eachturn_{step}_inst_level_*is weighted by that turn's instruction count, read from the checker's ownoutputs_strict['instruction_id_list'].parse_result([outputs])on a single turn had collapsed to that turn's own ratio, which the framework then macro-averaged.Docs
docs/{en,zh}/benchmarks/ifeval.mdand the adapterdescriptionthey are generated from now state the micro-average semantics.weighted_meanwas already declared inKNOWN_AGGREGATIONS(api/metric/semantics.py:36) andpoly_mathalready emits this identity, so the identity axis is unchanged and no semantics catalog entry is needed.Reproduction
The two reduction paths, using the shipped
agg_inst_level_accand theMeanbehaviour:The sign flips with the model's profile, which is why this can change relative ranking.
End to end through the real
ifevalchecker, with a 1-instruction prompt and a 3-instruction prompt where 1 of the 4 total instructions is followed:0.2500is the correct value: 1 followed instruction out of 4.numbecomes the instruction total, and the prompt-level metric is untouched.Validation
python -m pytest tests/metrics/aggregators/ -q # 31 passed (12 new, 19 existing)New coverage in
tests/metrics/aggregators/test_weighted_mean.py:numreporting the unit total so the cross-subset rollup stays microScoreScore.valuedropping that sample rather than scoring it 0aggregation='weighted_mean'ruff checkpasses on all touched paths.ruff format --checkreports whole-file rewrites for any file in acore.autocrlf=truecheckout because the configuredline-endingislf; verified clean by re-checking the touched files with LF endings.Open questions
inst_level_strict_macro) so historical results stay comparable? This PR changes the existing metric in place and adds nothing, on the assumption that matching the official definition is the intent.