147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
# 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,
|
|
}
|