# Copyright (c) ModelScope Contributors. All rights reserved. import numpy as np from transformers import EvalPrediction from typing import Dict from swift.utils import get_logger from .base import EvalMetrics from .utils import Metric logger = get_logger() class RerankerMetrics(EvalMetrics, Metric): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) Metric.__init__(self) self.add_state('logits', default_factory=list) self.add_state('labels', default_factory=list) def update(self, logits, labels): self.logits.append(logits.cpu().numpy()) self.labels.append(labels.cpu().numpy()) def compute(self): predictions = np.concatenate(self.logits) labels = np.concatenate(self.labels) return self._calculate_metrics(predictions, labels) def compute_metrics(self, eval_prediction: EvalPrediction) -> Dict[str, float]: return self._calculate_metrics(eval_prediction.predictions, eval_prediction.label_ids) def _calculate_metrics(self, logits, labels): """ Calculate MRR and NDCG metrics for reranker. This function first groups the data based on query boundaries (identified by positive samples), then calculates MRR and NDCG for each group independently, and finally returns the mean across all queries. Data format: - Each query group starts with a positive sample (label=1) followed by negatives (label=0) - Example: [1,0,0,1,0,0,0] represents 2 queries: query1=[1,0,0], query2=[1,0,0,0] Args: logits: Model output scores [batch_size] (numpy array or can be converted to numpy) labels: Binary labels (1 for positive, 0 for negative) [batch_size] Returns: dict: Dictionary containing MRR and NDCG metrics averaged across all queries """ # Convert to numpy if needed if hasattr(logits, 'numpy'): logits = logits.numpy() if hasattr(labels, 'numpy'): labels = labels.numpy() logits = np.array(logits).flatten() labels = np.array(labels).flatten() # Step 1: Find all positive sample indices (query boundaries) positive_indices = np.where(labels == 1)[0] if len(positive_indices) == 0: return {'mrr': 0.0, 'ndcg': 0.0} # Step 2: Split into groups (queries) query_groups = [] for i, pos_idx in enumerate(positive_indices): # Each group starts at a positive index group_start = pos_idx # Group ends at the next positive index or end of data if i + 1 < len(positive_indices): group_end = positive_indices[i + 1] else: group_end = len(labels) # Extract this query's data query_logits = logits[group_start:group_end] query_labels = labels[group_start:group_end] query_groups.append((query_logits, query_labels)) # Step 3: Calculate metrics for each query independently mrr_scores = [] ndcg_scores = [] for query_idx, (query_logits, query_labels) in enumerate(query_groups): # Skip groups that are too small (need at least 1 positive + 1 negative) if len(query_logits) < 2: logger.info(f'Query {query_idx}: Skipped (too small: {len(query_logits)} items)') continue # Verify that the first sample is positive (data format validation) if query_labels[0] != 1: logger.info(f'Query {query_idx}: Skipped (first sample not positive)') continue # Step 3a: Calculate ranking within this query ranking = np.argsort(-query_logits) # Sort by logits descending # Step 3b: Find position of positive document (should be at index 0 in query) pos_rank = np.where(ranking == 0)[0][0] + 1 # +1 for 1-based ranking # Step 3c: Calculate MRR for this query mrr = 1.0 / pos_rank mrr_scores.append(mrr) # Step 3d: Calculate NDCG for this query def calculate_ndcg_single_query(relevance_scores, ranking): """Calculate NDCG for a single query""" # Calculate DCG (Discounted Cumulative Gain) dcg = 0.0 for rank_pos, doc_idx in enumerate(ranking): relevance = relevance_scores[doc_idx] dcg += (2**relevance - 1) / np.log2(rank_pos + 2) # rank_pos+2 because log2(1) undefined # Calculate IDCG (Ideal DCG) ideal_relevance = np.sort(relevance_scores)[::-1] # Sort relevance descending idcg = 0.0 for rank_pos, relevance in enumerate(ideal_relevance): idcg += (2**relevance - 1) / np.log2(rank_pos + 2) # NDCG = DCG / IDCG if idcg == 0: return 0.0 return dcg / idcg # Create relevance scores (1 for positive, 0 for negative) relevance_scores = query_labels.astype(float) ndcg = calculate_ndcg_single_query(relevance_scores, ranking) ndcg_scores.append(ndcg) # Step 4: Calculate mean metrics across all valid queries if len(mrr_scores) == 0: logger.warning('No valid queries found for metric calculation') return {'mrr': 0.0, 'ndcg': 0.0} mean_mrr = np.mean(mrr_scores) mean_ndcg = np.mean(ndcg_scores) return { 'mrr': mean_mrr, 'ndcg': mean_ndcg, }