1616
1717from __future__ import annotations
1818
19- from typing import Any , Mapping
19+ from typing import Any , Literal , Mapping
2020
2121import torch
22+ from PIL import Image
2223
2324from pruna .evaluation .metrics .metric_qa_accuracy import QAAccuracyMetric
2425from pruna .evaluation .metrics .registry import MetricRegistry
2526from pruna .evaluation .metrics .utils import metric_data_processor
26- from pruna .evaluation .metrics .vlm_utils import _process_images
27+ from pruna .evaluation .metrics .vlm_utils import _process_images , split_mxn_grid
28+
29+ _DEFAULT_ONEIG_ALIGNMENT_VLM = "Qwen/Qwen2.5-VL-7B-Instruct"
2730
2831
2932def _int_dict_keys (mapping : Mapping [Any , Any ]) -> dict [int , Any ]:
@@ -122,73 +125,146 @@ def aggregate_oneig_alignment_per_cell(filtered_scores: Mapping[int, float], que
122125 return s / float (len (question_ids ))
123126
124127
128+ def _aux_list_from_gt (aux_slot : Any , batch_size : int ) -> list [dict [str , Any ]]:
129+ if isinstance (aux_slot , torch .Tensor ):
130+ raise ValueError (
131+ "oneig_alignment expects gt as list[dict] with 'questions' and optional 'dependencies'. "
132+ f"Got tensor with shape { tuple (aux_slot .shape )} ."
133+ )
134+ if not isinstance (aux_slot , (list , tuple )):
135+ return [{} for _ in range (batch_size )]
136+ out : list [dict [str , Any ]] = []
137+ for i in range (batch_size ):
138+ row = aux_slot [i ] if i < len (aux_slot ) else {}
139+ if not isinstance (row , dict ):
140+ raise ValueError (f"oneig_alignment requires aux[{ i } ] to be a dict. Got: { type (row )!r} ." )
141+ out .append (row )
142+ return out
143+
144+
125145@MetricRegistry .register ("oneig_alignment" )
126146class OneIGAlignmentMetric (QAAccuracyMetric ):
127147 """
128148 OneIG alignment with dependency-aware aggregation.
129149
130- Reuses :class:`QAAccuracyMetric` VLM Yes/No scoring but aggregates like
131- ``OneIG-Benchmark`` ``alignment_score.py`` for a **single** grid cell (no
132- ``split_mxn_grid``): question ids are sorted numerically, raw scores are
133- masked when any non-root parent is ``No``, then the mean over all questions
134- is stored per image. Entries with null or blank question text (HF ``datasets``
135- schema padding) are omitted from scoring.
150+ Matches ``OneIG-Benchmark`` ``alignment_score.py``: split an ``m x n`` output grid
151+ (default ``2 x 2``), score **one question per VLM call** across all cells, apply
152+ dependency masking per cell, then average cell scores.
136153
137- Numerical parity with upstream also depends on the VLM (e.g. ``openai/gpt-4o`` via
138- litellm vs reference Qwen2.5-VL).
154+ Scoring semantics
155+ -----------------
156+ OneIG Q_D probes are phrased so **Yes = aligned**. Each call requests
157+ :meth:`~pruna.evaluation.metrics.vlm_base.BaseVLM.score` with expected answer
158+ ``"Yes"`` (probability of Yes). Low scores act as semantic **No** for dependency
159+ masking.
139160
140161 Parameters
141162 ----------
142- *args : Any
143- Additional positional arguments for :class:`QAAccuracyMetric`.
163+ grid_size : tuple[int, int], optional
164+ ``(columns, rows)`` for :func:`~pruna.evaluation.metrics.vlm_utils.split_mxn_grid`.
165+ Default ``(2, 2)`` per OneIG. Use ``(1, 1)`` to score the full image without splitting.
144166 vlm : BaseVLM | None, optional
145167 Custom VLM instance. If provided, ``vlm_type`` and ``model_name`` are ignored.
146168 vlm_type : {"litellm", "transformers"}, optional
147- VLM backend. Default is ``"litellm "``.
169+ VLM backend. Default is ``"transformers "`` (paper-faithful Qwen2.5-VL) .
148170 model_name : str | None, optional
149- Litellm model id or HuggingFace checkpoint id. **Required** when ``vlm`` is not
150- provided (e.g. ``openai/gpt-4o``).
171+ HuggingFace or litellm model id. Default ``Qwen/Qwen2.5-VL-7B-Instruct``.
151172 vlm_kwargs : dict, optional
152- Forwarded by ``get_vlm`` to ``LitellmVLM`` or ``TransformersVLM``. For local models,
153- set ``model_load_kwargs`` for ``from_pretrained``; for litellm, pass extra API options.
173+ Forwarded by ``get_vlm``.
154174 structured_output : bool, optional
155- Use structured generation (litellm pydantic; transformers outlines when applicable).
156- Default is True.
175+ Use structured generation when applicable.
157176 device : str | torch.device | None, optional
158177 Device for transformers VLM.
159178 api_key : str | None, optional
160179 API key for litellm.
161180 call_type : str, optional
162181 Call type for the metric.
182+ aggregation : str, optional
183+ Unused; kept for registry compatibility with :class:`QAAccuracyMetric`.
163184 **kwargs : Any
164185 Additional keyword arguments for :class:`QAAccuracyMetric`.
165186
166187 Examples
167188 --------
168- Same ``hosted`` / ``local`` pattern as ``QAAccuracyMetric`` and
169- :func:`~pruna.evaluation.metrics.vlm_base.get_vlm`:
170-
171189 .. code-block:: python
172190
173- import torch
174-
175191 from pruna.evaluation.metrics import OneIGAlignmentMetric
176192
177- hosted = OneIGAlignmentMetric(vlm_type="litellm", model_name="openai/gpt-4o")
178- local = OneIGAlignmentMetric(
179- vlm_type="transformers",
180- model_name="HuggingFaceTB/SmolVLM-256M-Instruct",
181- device="cpu",
182- vlm_kwargs={"model_load_kwargs": {"torch_dtype": torch.float32}},
183- )
193+ paper = OneIGAlignmentMetric(device="cuda")
194+ api = OneIGAlignmentMetric(vlm_type="litellm", model_name="openai/gpt-4o")
184195 """
185196
186197 metric_name : str = "oneig_alignment"
187198 metric_units : str = "alignment"
188199
200+ def __init__ (
201+ self ,
202+ * args : Any ,
203+ grid_size : tuple [int , int ] = (2 , 2 ),
204+ vlm : Any | None = None ,
205+ vlm_type : Literal ["litellm" , "transformers" ] = "transformers" ,
206+ model_name : str | None = _DEFAULT_ONEIG_ALIGNMENT_VLM ,
207+ vlm_kwargs : dict | None = None ,
208+ structured_output : bool = True ,
209+ device : str | torch .device | None = None ,
210+ api_key : str | None = None ,
211+ call_type : str | None = None ,
212+ ** kwargs : Any ,
213+ ) -> None :
214+ super ().__init__ (
215+ * args ,
216+ vlm = vlm ,
217+ vlm_type = vlm_type ,
218+ model_name = model_name ,
219+ vlm_kwargs = vlm_kwargs ,
220+ structured_output = structured_output ,
221+ device = device ,
222+ api_key = api_key ,
223+ call_type = call_type if call_type is not None else "y_gt" ,
224+ ** kwargs ,
225+ )
226+ self .grid_size = (int (grid_size [0 ]), int (grid_size [1 ]))
227+
228+ def _score_sample (self , image : Any , aux : dict [str , Any ]) -> float :
229+ if not isinstance (image , Image .Image ):
230+ if isinstance (image , torch .Tensor ):
231+ from pruna .evaluation .metrics .vlm_utils import _tensor_to_pil
232+
233+ image = _tensor_to_pil (image )
234+ else :
235+ image = Image .fromarray (image ).convert ("RGB" )
236+ cells = split_mxn_grid (image , self .grid_size )
237+ qs = aux .get ("questions" )
238+ if not isinstance (qs , dict ) or not qs :
239+ raise ValueError (
240+ f"oneig_alignment requires 'questions' as a non-empty dict on aux. Got keys: { list (aux .keys ())} ."
241+ )
242+ qmap = _int_dict_keys (qs )
243+ qids = _active_oneig_question_ids (qmap )
244+ if not qids :
245+ return 0.0
246+ deps = _normalize_dependencies (aux .get ("dependencies" , {}))
247+ per_question_cell_scores : dict [int , list [float ]] = {}
248+ n_cells = len (cells )
249+ for qid in qids :
250+ qtext = str (qmap [qid ])
251+ raw_scores_list = self .vlm .score (
252+ cells ,
253+ [qtext ] * n_cells ,
254+ ["Yes" ] * n_cells ,
255+ response_format = self .response_format ,
256+ )
257+ per_question_cell_scores [qid ] = [float (s ) for s in raw_scores_list ]
258+ cell_means : list [float ] = []
259+ for cell_i in range (n_cells ):
260+ raw_map = {qid : per_question_cell_scores [qid ][cell_i ] for qid in qids }
261+ filtered = apply_oneig_dependency_mask (raw_map , deps )
262+ cell_means .append (aggregate_oneig_alignment_per_cell (filtered , qids ))
263+ return float (sum (cell_means ) / len (cell_means ))
264+
189265 def update (self , x : list [Any ] | torch .Tensor , gt : torch .Tensor , outputs : torch .Tensor ) -> None :
190266 """
191- Score each question with the VLM, apply dependency masking, append per-cell mean .
267+ Score each prompt image with OneIG alignment (grid split + per-question VLM calls) .
192268
193269 Parameters
194270 ----------
@@ -202,33 +278,6 @@ def update(self, x: list[Any] | torch.Tensor, gt: torch.Tensor, outputs: torch.T
202278 """
203279 inputs = metric_data_processor (x , gt , outputs , self .call_type )
204280 images = _process_images (inputs [0 ])
205- aux_list = inputs [1 ] if len (inputs ) > 1 else []
206- if isinstance (aux_list , torch .Tensor ):
207- aux_list = aux_list .tolist ()
281+ aux_list = _aux_list_from_gt (inputs [1 ] if len (inputs ) > 1 else [], len (images ))
208282 for i , image in enumerate (images ):
209- aux = aux_list [i ] if i < len (aux_list ) else {}
210- if not isinstance (aux , dict ):
211- raise ValueError (
212- "oneig_alignment requires aux[{}] to be a dict with 'questions'. Got: {!r}." .format (i , type (aux ))
213- )
214- qs = aux .get ("questions" )
215- if not isinstance (qs , dict ) or not qs :
216- raise ValueError (
217- f"oneig_alignment requires 'questions' as a non-empty dict on aux. Got keys: { list (aux .keys ())} ."
218- )
219- qmap = _int_dict_keys (qs )
220- qids = _active_oneig_question_ids (qmap )
221- if not qids :
222- self .scores .append (0.0 )
223- continue
224- question_texts = [str (qmap [qi ]) for qi in qids ]
225- deps = _normalize_dependencies (aux .get ("dependencies" , {}))
226- raw_scores_list = self .vlm .score (
227- [image ] * len (question_texts ),
228- question_texts ,
229- ["Yes" ] * len (question_texts ),
230- response_format = self .response_format ,
231- )
232- raw_map = {qid : float (raw_scores_list [j ]) for j , qid in enumerate (qids )}
233- filtered = apply_oneig_dependency_mask (raw_map , deps )
234- self .scores .append (aggregate_oneig_alignment_per_cell (filtered , qids ))
283+ self .scores .append (self ._score_sample (image , aux_list [i ]))
0 commit comments