Spaces:
Sleeping
Sleeping
| """ | |
| evaluate.py — Compute metrics on the validation set. | |
| The competition uses Micro-F1 as the primary metric. Here we also | |
| compute per-aspect breakdowns so we can see exactly where the model | |
| is strong or weak. | |
| Adapted for the per-aspect softmax architecture (9 heads × 4 classes). | |
| """ | |
| import json | |
| import numpy as np | |
| import torch | |
| from torch.utils.data import DataLoader | |
| from sklearn.metrics import f1_score | |
| from tqdm import tqdm | |
| from src.config import ( | |
| DEVICE, BATCH_SIZE, ASPECTS, NUM_ASPECTS, | |
| IDX_TO_SENTIMENT, IDX_TO_ASPECT, | |
| VAL_FILE, MODEL_DIR, | |
| ) | |
| from src.preprocess import load_labeled_data, decode_labels | |
| from src.dataset import ABSADataset | |
| from src.model import ABSAModel | |
| def predict_all(model, dataloader, device): | |
| """ | |
| Run the model on a dataloader and collect all predictions. | |
| Returns | |
| ------- | |
| all_preds : np.ndarray (N, 9) — predicted class per aspect | |
| all_probs : list of np.ndarray — raw softmax probs per aspect | |
| all_labels : np.ndarray (N, 9) — ground truth class per aspect | |
| all_ids : list — review IDs | |
| """ | |
| model.eval() | |
| all_preds = [] | |
| all_labels = [] | |
| all_ids = [] | |
| all_aspect_probs = [[] for _ in range(NUM_ASPECTS)] | |
| with torch.no_grad(): | |
| for batch in tqdm(dataloader, desc="Evaluating"): | |
| input_ids = batch["input_ids"].to(device) | |
| attn_mask = batch["attention_mask"].to(device) | |
| labels = batch["labels"] # (batch, 9) int64 | |
| output = model(input_ids, attn_mask) | |
| probs_list = output["probs"] # list of 9 tensors, each (batch, 4) | |
| batch_preds = [] | |
| for i, probs in enumerate(probs_list): | |
| p = probs.cpu().numpy() # (batch, 4) | |
| all_aspect_probs[i].append(p) | |
| pred = np.argmax(p, axis=-1) # (batch,) | |
| batch_preds.append(pred) | |
| # Stack into (batch, 9) | |
| batch_preds = np.stack(batch_preds, axis=1) | |
| all_preds.append(batch_preds) | |
| all_labels.append(labels.numpy()) | |
| all_ids.extend(batch["review_id"].tolist()) | |
| all_preds = np.concatenate(all_preds, axis=0) | |
| all_labels = np.concatenate(all_labels, axis=0) | |
| # Concatenate per-aspect probs | |
| for i in range(NUM_ASPECTS): | |
| all_aspect_probs[i] = np.concatenate(all_aspect_probs[i], axis=0) | |
| return all_preds, all_aspect_probs, all_labels, all_ids | |
| def compute_micro_f1(y_true: np.ndarray, y_pred: np.ndarray) -> float: | |
| """ | |
| Compute Micro-F1 in the ABSA sense: | |
| An (aspect, sentiment) pair is a "positive example" if the aspect | |
| is mentioned (class != 0). A correct prediction must match BOTH | |
| the aspect being mentioned AND the correct sentiment. | |
| We convert back to 27-dim binary vectors (the original competition | |
| format) and compute micro-F1 on those. | |
| """ | |
| # Build 27-dim binary vectors for true and pred | |
| num_labels = NUM_ASPECTS * 3 # 9 aspects × 3 sentiments (excl. absent) | |
| y_true_bin = np.zeros((len(y_true), num_labels), dtype=int) | |
| y_pred_bin = np.zeros((len(y_pred), num_labels), dtype=int) | |
| for i in range(len(y_true)): | |
| for j in range(NUM_ASPECTS): | |
| true_class = y_true[i, j] | |
| pred_class = y_pred[i, j] | |
| # class 0 = not_mentioned, 1 = positive, 2 = negative, 3 = neutral | |
| if true_class > 0: | |
| idx = j * 3 + (true_class - 1) | |
| y_true_bin[i, idx] = 1 | |
| if pred_class > 0: | |
| idx = j * 3 + (pred_class - 1) | |
| y_pred_bin[i, idx] = 1 | |
| return f1_score(y_true_bin.flatten(), y_pred_bin.flatten(), average="binary") | |
| def evaluate_model(model=None, val_df=None, device=DEVICE, verbose=True): | |
| """ | |
| Full evaluation pipeline. If model/data not provided, loads from disk. | |
| """ | |
| import os | |
| if val_df is None: | |
| val_df = load_labeled_data(VAL_FILE) | |
| if model is None: | |
| model = ABSAModel() | |
| weights_path = os.path.join(MODEL_DIR, "best_model.pt") | |
| model.load_state_dict(torch.load(weights_path, map_location=device)) | |
| model = model.to(device) | |
| val_dataset = ABSADataset(val_df) | |
| val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False) | |
| preds, probs, labels, ids = predict_all(model, val_loader, device) | |
| f1 = compute_micro_f1(labels, preds) | |
| if verbose: | |
| print(f"\n{'='*60}") | |
| print(f" Micro-F1 (ABSA): {f1:.4f}") | |
| print(f"{'='*60}\n") | |
| # Per-aspect breakdown | |
| for i, aspect in enumerate(ASPECTS): | |
| true_mentioned = (labels[:, i] > 0).sum() | |
| pred_mentioned = (preds[:, i] > 0).sum() | |
| correct = ((labels[:, i] > 0) & (preds[:, i] == labels[:, i])).sum() | |
| if true_mentioned > 0: | |
| acc = correct / true_mentioned | |
| print(f" {aspect:20s}: mentioned={true_mentioned:4d}, " | |
| f"pred={pred_mentioned:4d}, correct={correct:4d}, " | |
| f"acc={acc:.3f}") | |
| return f1, preds, probs, labels | |
| if __name__ == "__main__": | |
| evaluate_model() | |